Node.js Env false Is Truthy: Parse Boolean Flags Explicitly
FEATURE_ENABLED=false does not make Boolean(process.env.FEATURE_ENABLED) false. The value is the string 'false', which is truthy. A feature guarded by if (process.env.FEATURE_ENABLED) can therefore run even though the configuration appears to disable it.
Parse the flag before using it. For a switch whose only accepted enabled value is the exact string true, use process.env.FEATURE_ENABLED === 'true'. If a typo should prevent startup, validate both accepted values and reject everything else.
Presence is different from a boolean setting
Node’s environment-variable documentation specifies that values loaded from .env are strings. Quotes in the file do not create a JavaScript boolean. JavaScript boolean conversion treats nonempty strings as truthy, including 'false' and '0'.
These expressions answer different questions:
const presentAndNonempty = Boolean(process.env.FEATURE_ENABLED);const explicitlyEnabled = process.env.FEATURE_ENABLED === 'true';The first checks truthiness; it does not interpret the word. The second implements an exact-value policy. It disables the feature for missing values, empty strings, TRUE, and tru as well as false. That can be an acceptable default, but it also hides invalid configuration.
For a startup configuration where invalid values should be visible, define the policy explicitly:
function readBoolean(env, name, fallback = false) { const value = env[name]; if (value === undefined) return fallback; if (value === 'true') return true; if (value === 'false') return false; throw new Error(`${name} must be true or false`);}
const featureEnabled = readBoolean(process.env, 'FEATURE_ENABLED');This parser deliberately accepts only lowercase true and false. Missing means the fallback; an empty string means an invalid supplied value. It does not accept 1, 0, yes, case variants, or surrounding spaces. If your deployment requires those forms, define and test that grammar instead of relying on truthiness.
Reproduce the result through actual .env loading
Save this complete fixture as env-flags.mjs and run node env-flags.mjs. It creates a temporary directory, writes one flag file per case, and launches Node child processes with --env-file. It removes the temporary directory afterward.
The fixture was executed on macOS with Node.js 24.13.0 on October 7, 2026. Each child receives a copy of the parent environment with this specific flag removed unless the case intentionally supplies an override. No hosting dashboard or third-party dotenv package was tested.
import assert from 'node:assert/strict';import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';import { tmpdir } from 'node:os';import { join } from 'node:path';import { spawnSync } from 'node:child_process';
function readBoolean(env, name, fallback = false) { const value = env[name]; if (value === undefined) return fallback; if (value === 'true') return true; if (value === 'false') return false; throw new Error(`${name} must be true or false`);}
const directory = mkdtempSync(join(tmpdir(), 'env-flags-'));const probe = join(directory, 'probe.mjs');writeFileSync(probe, `${readBoolean.toString()}const raw = process.env.FEATURE_ENABLED;let parsed;try { parsed = readBoolean(process.env, 'FEATURE_ENABLED'); }catch { parsed = 'invalid'; }console.log(JSON.stringify({ raw: raw ?? null, truthy: Boolean(raw), exact: raw === 'true', parsed,}));`);
const cases = [ ['false', 'FEATURE_ENABLED=false\n', undefined, 'false', true, false], ['true', 'FEATURE_ENABLED=true\n', undefined, 'true', true, true], ['missing', '# no flag\n', undefined, null, false, false], ['empty', 'FEATURE_ENABLED=\n', undefined, '', false, 'invalid'], ['zero', 'FEATURE_ENABLED=0\n', undefined, '0', true, 'invalid'], ['typo', 'FEATURE_ENABLED=tru\n', undefined, 'tru', true, 'invalid'], ['parent', 'FEATURE_ENABLED=false\n', 'true', 'true', true, true],];
try { for (const [label, content, override, raw, truthy, parsed] of cases) { const file = join(directory, `${label}.env`); writeFileSync(file, content); const env = { ...process.env }; delete env.FEATURE_ENABLED; if (override !== undefined) env.FEATURE_ENABLED = override; const child = spawnSync(process.execPath, [`--env-file=${file}`, probe], { env, encoding: 'utf8', }); assert.ifError(child.error); assert.equal(child.status, 0, child.stderr); const result = JSON.parse(child.stdout); assert.deepEqual(result, { raw, truthy, exact: raw === 'true', parsed }); console.log(`${label}: ${JSON.stringify(result)}`); }} finally { rmSync(directory, { recursive: true, force: true });}The assertions pass with this output:
false: {"raw":"false","truthy":true,"exact":false,"parsed":false}true: {"raw":"true","truthy":true,"exact":true,"parsed":true}missing: {"raw":null,"truthy":false,"exact":false,"parsed":false}empty: {"raw":"","truthy":false,"exact":false,"parsed":"invalid"}zero: {"raw":"0","truthy":true,"exact":false,"parsed":"invalid"}typo: {"raw":"tru","truthy":true,"exact":false,"parsed":"invalid"}parent: {"raw":"true","truthy":true,"exact":true,"parsed":true}raw: null is the fixture’s display marker for an absent variable; it is not the actual value in process.env. The probe catches validation errors to display all cases. In application startup, allow that error to stop initialization before the flag controls any work.
If parsing is correct but the feature still runs
The last case puts false in the file and true in the child environment. The effective value is true. Node’s --env-file documentation specifies that an existing environment value takes precedence over the file.
Check the effective value in the process that actually runs the application. For this nonsecret flag, a temporary diagnostic can print JSON.stringify(process.env.FEATURE_ENABLED) to distinguish an absent value, an empty string, and a supplied string. Avoid dumping the entire environment because it can contain credentials.
A strict parser cannot determine which source you intended to win: the parent override is valid true. Correct the supplying environment and start a new process with the intended setting. If application code stores the parsed boolean at startup, later mutation of process.env will not recalculate that stored value.
This article covers Node runtime flags. A browser bundle using Vite has a separate loading and replacement boundary; see Vite environment variables undefined in production for that diagnosis.
Use exact comparison when unknown values should disable the feature. Use a validating parser when unknown values should fail startup. In either case, verify the effective string before investigating the code behind the switch.
References: