---
title: Loading
description: What load() reads in production and on your machine, which value wins, and how errors look.
order: 1
---

| situation | what `load()` does |
| --- | --- |
| `ENV_SECRET` is set (a deployed service) | reads `/run/secrets/$ENV_SECRET`. A missing file, a directory, or a file that does not parse **throws**, and nothing from it is set |
| `ENV_SECRET` is unset or empty (your machine, tests) | reads `.env` in the working directory if there is one. A missing file is fine; a broken one prints a warning and sets nothing |

`SECRETS_DIR` moves the directory the secret is read from, for a setup that mounts secrets
somewhere other than `/run/secrets`. Parent directories are never searched for `.env`.

## What wins

A variable already in the environment wins, even when it is empty. Within the file, the
first of a repeated name wins. That is how a plain container variable, such as the
`DATABASE_URL` of a SQLite database the service mounts, overrides a group key of the same
name.

## A failed load stops the process

Under `--import` or `--preload`, a thrown error stops Node or Bun before your app starts.
That is the point: a service that boots with half its configuration silently absent is
worse than one that does not boot.

```text
EnvFileError: ENV_SECRET names 'web-env' but /run/secrets/web-env is not there: the deploy did not render this service's env secret
```

Errors name the file and the line, never the line's content, because that content is a
secret and the error lands in the boot log.

## For tests

```ts
import { loadFrom } from '@syncronet/ops-env'

const env: Record<string, string | undefined> = {}
loadFrom('web-env', '/tmp/fixtures', env, process.cwd())
```

`loadFrom(secretName, secretsDir, env, cwd)` does what `load()` does into an object you
pass. `parse(text, source)` returns the `[name, value]` pairs of a file and throws
`EnvParseError` on a broken one.
