Skip to content

fix(process): #1312 unset process.env.X is undefined (nullish), so ?? applies#1314

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-1312-process-env-nullish
May 22, 2026
Merged

fix(process): #1312 unset process.env.X is undefined (nullish), so ?? applies#1314
proggeramlug merged 1 commit into
mainfrom
worktree-fix-1312-process-env-nullish

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Fixes #1312. Reading an unset env var via process.env.X (or process.env[key]) returned a value that was falsy but not nullish, so the idiomatic process.env.X ?? default silently kept the bogus value instead of applying the fallback.

Root cause

The process.env.X / process.env[key] fast paths (EnvGet / EnvGetDynamic in codegen) called js_getenv, which returns a null *StringHeader when the var is unset, then unconditionally tagged the result with STRING_TAG. A null pointer OR'd with STRING_TAG is a malformed value:

Operation Result Correct?
typeof "string" ✗ (Node: "undefined")
JSON.stringify null ✗ (Node: undefined)
?? default non-nullish → no fallback
|| default falsy → fallback ✓ (the documented workaround)

Fix

Add js_getenv_value(name) -> f64 in perry-runtime/src/process.rs that returns NaN-boxed undefined for an unset var — matching Node, where process.env.UNSET is undefined — and the string otherwise. A var set to the empty string still returns "" (falsy but not nullish), so ?? won't clobber a legitimately empty value. Both the static (EnvGet) and dynamic (EnvGetDynamic) codegen paths now call it. js_getenv is left intact.

Validation

  • New regression test test-files/test_issue_1312_env_nullish.ts covers unset / set / empty-string / dynamic-access cases. Perry output is byte-for-byte identical to node --experimental-strip-types.
  • test_process_env.ts parity still matches.
  • cargo test -p perry-runtime -p perry-codegen: 456 passed, 0 failed.
  • cargo fmt --all -- --check: clean.

Per repo convention this PR omits the version bump / CHANGELOG entry — the maintainer folds those in at merge.

… applies

Reading an unset env var via the process.env.X / process.env[key] fast
paths called js_getenv (returns a null *StringHeader when unset) and then
unconditionally tagged the result with STRING_TAG. A null pointer OR'd
with STRING_TAG is a malformed value: typeof reads "string", JSON.stringify
emits null, it is falsy under || but NOT nullish under ??, so the idiomatic
process.env.X ?? default silently kept the bogus value instead of the
fallback.

Add js_getenv_value(name) -> f64 which returns NaN-boxed undefined for an
unset var (matching Node, where process.env.UNSET is undefined) and the
string otherwise. A var set to the empty string still returns "" (falsy
but not nullish), so ?? won't clobber a legitimately empty value. Both the
static (EnvGet) and dynamic (EnvGetDynamic) codegen paths now call it.

Output is byte-for-byte identical to node --experimental-strip-types across
unset / set / empty-string / dynamic-access cases (test_issue_1312_env_nullish.ts).
@proggeramlug proggeramlug merged commit c959823 into main May 22, 2026
9 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-1312-process-env-nullish branch May 22, 2026 08:17
proggeramlug added a commit that referenced this pull request May 22, 2026
…sweep (#1414)

Rolls up 26 PRs that merged to main post-v0.5.1023 without version
bumps:

- node:crypto gap-fixes (#1386 #1393 #1394 #1402 #1405): randomInt,
  timingSafeEqual, getHashes/getCiphers, sha224/sha384, base64 digest,
  Buffer hash input, no-arg digest() → Buffer, pbkdf2Sync digest arg,
  scryptSync.
- node:perf_hooks (#1321 + #1328 #1342 coverage): performance + User
  Timing + PerformanceObserver native impl, granular node-suite +
  edge-case coverage.
- #1090 GC checkpoint runtime work (#1324).
- #1311 geisterhand on iOS (#1316 #1383 #1384 #1385).
- #1312 process.env.X (unset) is nullish undefined (#1314).
- #1319 thread-safety hardening for cross-thread runtime statics.
- #1322 exact-head GC evidence packet.
- #1323 wasm timers dispatch through mem_call bridge (#1329).
- #1317 node:timers/promises shadow-segfault fix (#1326).
- #1330 node:process suite (#1331).
- #1292 bcrypt.hash() returns String (#1307).
- #1293 fastify .json()/.body external-fastify dispatch (#1308).
- #1296 app pattern performance gaps.
- #1297 diagnostics_channel parity.
- #1301 iOS App Groups capability (#1313).
- #1318 #1325 os/methods/modern-methods static dispatch.
- #1315 expanded Node parity test coverage.
- #1382 ui-ios stdlib pump for async fetch.
- #1392 ui-wasm reactive state + setText (#1404).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

process.env.<UNSET> is non-nullish, breaking process.env.X ?? default

1 participant