# Reading values

Read one environment variable as a string or through a converter, with a fallback, and see what each reader returns for a bad value.

A reader is a method on `Envapter` that reads one variable, like `get` for a string or `getAs` for a typed value. On Node, readers read `process.env` and your [`.env` files](/docs/env-files). You call every reader on `Envapter` itself, so you construct nothing. Most readers return the value, or `undefined` when it is missing. The [required readers](/docs/required-values) throw. On Workers, the browser, and edge runtimes, every reader throws [`NoSourceBound`](/docs/errors) until you bind a [source](/docs/sources) with `useSource`.

## Strings and typed values

Without a reader you write the truthy list for flags yourself, and you remember that an empty string means unset. `getAs` with `Converters.Boolean` reads `1`, `yes`, `true`, and `on` as true and `0`, `no`, `false`, and `off` as false, in any case. `get` returns the value as a string, and `getAs` returns the type its [converter](/docs/converters) produces. Both take a key and an optional fallback.

```ts
const appName = Envapter.get('APP_NAME', 'envapt');
const sampleRate = Envapter.getAs('SAMPLE_RATE', Converters.Number, 0.25);
const debug = Envapter.getAs('DEBUG', Converters.Boolean, false);
const maxUploadBytes = Envapter.getAs('MAX_UPLOAD_BYTES', Converters.BigInt, 0n);
```

`0n` is the fallback, so `maxUploadBytes` is a plain `bigint`. `sampleRate` uses `Converters.Number` because `0.25` is a fraction. That converter also accepts `3.5` or `99999` as a port. `Converters.Port` takes only whole numbers from 0 to 65535, and `Converters.Integer` takes only whole numbers. [Converters](/docs/converters) lists the rest.

## Fallbacks and missing values

Without a fallback, a reader's type includes `undefined`, so you handle that at every call site. A fallback removes `undefined` from the type.

```ts
const withFallback = Envapter.get('API_URL', 'http://localhost');

const noFallback = Envapter.get('API_URL');
```

`withFallback` is a `string` because `'http://localhost'` supplies a value when `API_URL` is missing. `noFallback` can be `undefined`.

A variable is missing when it is unset or empty. In strict mode (`Envapter.strict = true`) a value of only spaces is missing too. With strict mode off, it counts as a value, so `Converters.Number` reads it as `0` ([Required values](/docs/required-values#strict-mode) covers strict mode).

The readers differ when a value is set but can't convert.

| Reader | Result for a value it can't convert |
| --- | --- |
| `getAs` with a built-in [converter](/docs/converters) | the fallback, or `undefined` without one |
| `getAs` with [`Converters.array`](/docs/converters#arrays) | throws [`ArrayElementConversionFailed`](/docs/errors) for a bad element, even with a fallback. In strict mode an empty element throws [`EmptyArrayElement`](/docs/errors) |
| `getAs` with your own function | if the function returns `undefined` or `null`, `getAs` returns the fallback, or `undefined` without one. An error it throws reaches your code unchanged |
| `getAs` with a [schema](/docs/standard-schema) | throws a schema error such as [`SchemaValidationFailed`](/docs/errors), and the fallback is not used |
| [`getRequired`, `getRequiredAll`](/docs/required-values) | throws [`InvalidEnvValue`](/docs/errors) |

A fallback can hide a typo. The `sampleRate` read above returns `0.25` for `SAMPLE_RATE=high`, and nothing reports it unless you set `Envapter.debug = 'verbose'` ([debug logging](/docs/errors#debug-logging)). If a wrong value has to stop the app, read it with [`getRequired`](/docs/required-values) instead.

If a fallback has the wrong type for its converter, TypeScript reports it before you run the code, as with `'abc'` for `Converters.Port`. A time fallback is milliseconds or a string like `'15m'`, so `'5'` is an error too. In plain JavaScript, or when a cast gets past the types, the call throws instead. The error is `FallbackConverterTypeMismatch` or `MalformedTimeFallback`, and it throws even when the variable is set. [Errors and debugging](/docs/errors) has every code.

## Ordered keys

Every reader except `getRequiredAll` accepts a list of keys. The reader uses the first key that has a value, so a renamed variable can keep its old name as a backup in the same read. It skips a key that is unset or empty, while `??` skips only an unset one.

```ts
// DATABASE_URL if it has a value, else the name older deploys still set
const databaseUrl = Envapter.getAs(['DATABASE_URL', 'LEGACY_DATABASE_URL'], Converters.Url);
```

> **WARNING**
>
> If you declare the list on its own, it widens to `string[]`, which a reader's key parameter rejects. Write the list inline, or add `as const`.
>
> ```ts
> const keys = ['DATABASE_URL', 'LEGACY_DATABASE_URL'] as const;
> ```

## Other types with getAs

Without `getAs`, you check for a missing variable, apply the default, and call `new URL(raw)` yourself, which throws on a bad value. `getAs` takes a converter, which can be:

- a built-in converter such as `Converters.Url` or `Converters.Time`.
- `Converters.array(...)`, which splits a value on commas into a list, converting each element with the `of` converter you pass.
- your own `(raw: string) => T` function, which runs only when the variable has a value.
- a [Standard Schema](/docs/standard-schema) validator, such as one from zod, valibot, or arktype.

```ts
const origins = Envapter.getAs('ALLOWED_ORIGINS', Converters.array({ of: Converters.Url }));

function parseCidr(raw: string) {
    const [network = '', bits = '32'] = raw.split('/');
    return { network, bits: Number(bits) };
}

const trustedProxies = Envapter.getAs('TRUSTED_PROXY_CIDR', parseCidr, { network: '127.0.0.1', bits: 32 });
```

`origins` is a `URL[]`, or `undefined` while `ALLOWED_ORIGINS` is unset. `parseCidr` turns `10.0.0.0/8` into `{ network: '10.0.0.0', bits: 8 }`, a shape no built-in converter produces.

## The raw string

Every other reader expands `${NAME}` references inside the value ([Templates](/docs/templates) explains the syntax). Expansion breaks a value your own code sets later, like an email subject holding `${ORDER_ID}`. If a variable named `ORDER_ID` exists, envapt substitutes its value. In strict mode, a reference to a missing variable throws. `getRaw` returns the string exactly as stored. It takes a key or an ordered key list and no fallback. To default an unset key, write `Envapter.getRaw(key) ?? 'default'`. It returns `undefined` when the key is unset and an empty string when the variable is empty.

```ts
// ORDER_EMAIL_SUBJECT=Your order ${ORDER_ID} has shipped
const subject = Envapter.getRaw('ORDER_EMAIL_SUBJECT');
```

`subject` keeps the `${ORDER_ID}` text, ready for your own code to replace.

## Extending Envapter

Every reader is a static method, so a class that extends `Envapter` inherits all of them. You call them as `Config.get(...)`. A subclass is worth writing when one setting combines several reads. Decorated fields work on any class, so a config class that holds only decorated fields doesn't need to extend `Envapter`.

```ts
class Config extends Envapter {
    static get origin(): string {
        const host = Config.get('HOST', 'localhost');
        const port = Config.getAs('PORT', Converters.Port, 3000);
        return `http://${host}:${port}`;
    }
}

Config.origin;
```

`Config.origin` converts again on every access, while a [decorated field](/docs/decorators) keeps the value from its first read until the [cache](/docs/sources#when-values-are-read) is rebuilt.

`Envapter` stores `strict`, the bound source, and the cache, so a subclass shares them. If you set `Config.strict = true`, strict mode turns on for every read in the process.

## More readers

- [Required values](/docs/required-values): `getRequired` and `getRequiredAll` throw when a value is missing. `require` throws if any listed key has no value, and `has` returns whether a key has one.
- [Templates](/docs/templates): `resolve` builds a string from several keys.
- [Environment](/docs/environment): `isProduction`, `isDevelopment`, and the other environment checks.