Required values
Stop the app at boot when a variable is missing or malformed, and see every missing key, or every malformed key, in one error.
If a value is wrong, a plain read often returns something and the app keeps running. Envapter.getAs('DATABASE_URL', Converters.Url) returns undefined for a missing variable, so the app fails later, in whatever request touches the database first. A required read throws on a bad value, while a plain read of a token or function returns the fallback or undefined. getRequired, getRequiredAll, and a decorated field with required: true are required reads. require is one too, but it only checks that each value is present. If the read runs at boot, a bad deploy stops the app before it serves anything.
With one required read per variable, only the first bad value threw. You fixed it, deployed again, and hit the next one. getRequiredAll reads the whole group and reports every missing key in one error, or every malformed key in one error:
export const = .(
{
: .,
: .,
: .,
: .({ : . }),
: .(['debug', 'info', 'warn', 'error'])
},
'camelCase'
);
export const port = .;The object you pass is the spec. Each key is an env name, and each value is a token, an array() converter, your own converter function, or a schema like the LOG_LEVEL enum. A spec key is a single env name. The returned record holds every value already converted. None of the fields read with a token or function can be undefined, which is why port is a number. A schema field holds whatever its schema returns.
The optional second argument renames the record's keys. envapt splits each name on underscores:
| Casing | DATABASE_URL becomes |
|---|---|
| none | DATABASE_URL |
'camelCase' | databaseUrl |
'PascalCase' | DatabaseUrl |
'kebab-case' | database-url |
It also lowercases each word after its first letter. MY_KEY becomes myKey under 'camelCase', while a mixed-case name like myKey has no underscore and becomes mykey.
Missing and invalid are different failures
MissingEnvValue (305) means the variable is unset or empty. InvalidEnvValue (210) means it's set but the converter or schema rejected it. The two codes let a catch tell them apart.
getRequiredAll checks in a fixed order:
getRequiredAll(spec)
├─ a bad converter in the spec ... throws first (a mistyped token is 202)
├─ any key missing ............... one MissingEnvValue, every missing key
└─ every key present
└─ any failed ................. one InvalidEnvValue, every failed keyThe order has a cost. If one key is missing and another is malformed, you see only the missing key. After you set it, the next run reports the malformed one. A run never mixes the two codes.
Strict mode adds one exception. If a value holds an unresolved ${VAR}, require and getRequiredAll throw MissingEnvValue for that key right away. Keys that were missing earlier go unreported, and later keys go unchecked.
If DATABASE_URL, PORT, and LOG_LEVEL are unset, the example above throws:
EnvaptError [305]: Missing required environment variables: DATABASE_URL, PORT, LOG_LEVEL.If every variable is set but three values are malformed, among them ALLOWED_ORIGINS=https://app.example.com,example.com, it throws:
EnvaptError [210]: Validation failed for DATABASE_URL, PORT, ALLOWED_ORIGINS.Reading the error without leaking values
The MissingEnvValue and InvalidEnvValue messages list only keys. An InvalidEnvValue also carries error.issues, an array of issues in the Standard Schema shape. Each has a message and a path that starts with the env key. For the 210 run above, issues holds:
[
{ "message": "could not be converted", "path": ["DATABASE_URL"] },
{ "message": "could not be converted", "path": ["PORT"] },
{
"message": "Element \"example.com\" at index 1 could not be converted to url.",
"path": ["ALLOWED_ORIGINS"]
}
]The third issue quotes example.com. An issue message can quote the value it rejected, and a value can be a secret, such as the password inside DATABASE_URL. The error text leaves the messages out for that reason. Node still prints an uncaught error with its properties, so an uncaught InvalidEnvValue prints issues and those messages. To keep them out of your logs, catch the error, log its message, and exit:
function () {
try {
return .({
: .,
: .
});
} catch () {
if ( instanceof ) {
.(.);
.(1);
}
throw ;
}
}error.message is the line below, which carries every key you need.
Validation failed for DATABASE_URL, PORT.Log error.issues only when you know your converters and schemas never quote a value. The sample exits with process.exit(1) on Node, Bun, and Deno.
getRequired, require, and has
| Reader | Returns | When the value is missing | When it can't convert |
|---|---|---|---|
getRequired(key, converter) | the converted value | throws MissingEnvValue | throws InvalidEnvValue |
getRequiredAll(spec, casing?) | a record of values | one MissingEnvValue for every missing key | one InvalidEnvValue for every bad key |
require(key, ...keys) | nothing | one MissingEnvValue for every missing key | not checked |
has(key) | a boolean | false | not checked |
getRequired takes the same converters and schemas as getRequiredAll. Its key can also be an ordered key list, which a spec key can't. For a plain string, pass Converters.String. A bad converter throws before MissingEnvValue does.
require converts nothing, so use it for values that other code reads later, such as a decorated class. Each argument can be an ordered key list too, and an argument passes if any of its keys has a value:
.('API_KEY', 'SESSION_SECRET', ['READONLY_URL', 'DATABASE_URL']);This passes when API_KEY and SESSION_SECRET have values and at least one of READONLY_URL and DATABASE_URL does.
has takes an ordered key list as well. It treats an unset or empty value as missing, and a whitespace-only value too when strict mode is on, the same as a required read. If you call has before getRequired, the value is optional but still checked when it is set:
export function (): | undefined {
if (!.('SENTRY_DSN')) return ;
return .('SENTRY_DSN', .);
}sentryDsn returns undefined for an unset SENTRY_DSN but throws for a malformed one. A getAs would return its fallback, or undefined, and hide the typo.
Where to put the check
Put getRequiredAll at the top level of one config module that your entry file imports first. Module code runs at import, so the throw happens before the app serves anything. Every other file reads from the exported config record. The cost is that every test importing that module needs the variables set first. Set them in a setup file that runs before the import. A library, or a CLI whose --help must work without config, should read inside a function and skip this.
On Workers, the browser, and edge runtimes, call useSource before the read, in the same module, because a read before it throws NoSourceBound.
A decorated field with required: true reads the first time something accesses it, so a missing value only throws when that happens. Read the field once at startup to catch it earlier.
Strict mode
A variable counts as missing when it's unset or empty. Strict mode adds whitespace-only values to that rule and changes the other behaviors in the table below. It's off by default and applies to every read in the process.
Without it, a value of only spaces counts as set. That happens when a CI secret expands to spaces, or when a .env file quotes the value, as in API_KEY=" ". Envapter.require('API_KEY') passes, and getRequired with Converters.String returns the spaces. Strict mode makes both throw MissingEnvValue.
. = true;When you set it, envapt rebuilds the cache from the bound source. On Workers, the browser, and edge runtimes, you can set it before useSource too, and useSource applies it.
Strict mode also applies to templates and the resolve tagged template. An ordered key list skips a whitespace-only key and uses the next one.
Every later read follows the new rule. A value you already read into a variable keeps its old result, so set it before those reads. With strict mode on, a plain reader still returns the fallback for an unset or whitespace-only value. It throws for an unresolved ${VAR} in any reader that expands templates, and for an empty array element in any array read.
Turn it on if your values come from files with ${VAR} templates or from CI secrets, because a blank value there is probably a mistake. The cost is that a stray trailing comma in a deployed .env makes a startup read throw EmptyArrayElement. Leave it off if a whitespace-only value is meaningful in your app.
Validating with a schema
Check an environment value against a zod, valibot, or arktype schema and get the schema's output type back, with the failed rule in the error's issues.
Templates
Build one environment variable from others by referencing them inside its value, and assemble strings from several keys with the resolve() tagged template.