__esm helper silently swallows init errors after the first throw (original) (raw)
Version
esbuild 0.25.4. The pattern below has been present in the helper for a long time and reproduces on every recent version I checked.
Summary
The __esm helper used to lazy-initialize ESM-namespace modules surfaces a thrown init error only on the very first call. All subsequent calls return undefined silently, so downstream code throws a misleading TypeError: Cannot read properties of undefined (reading '<some-export>') and the real init error is gone.
Root cause: fn = 0 is sequenced before the call to the underlying init function. If the init throws, fn has already been cleared, so the next time the helper runs, the fn && ... short-circuit skips the init entirely and returns the still-unset res = undefined.
Helper as emitted
var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; };
The order of operations on the call line:
__getOwnPropNames(fn)[0]— read the first property name offnfn[...]— fetch the init function(0, ...)— stripthis(fn = 0)— assignment passed as the argument;fnis now0before the call returns(...)— call the init function with0as the arg- If init throws, control jumps out with
fnalready 0
After step 6, res is still undefined. Every later call returns undefined.
Minimal repro
// boom.js console.log("evaluating boom.js"); throw new Error("init failed (the real cause)"); export const value = 42;
// consumer.js import { value } from "./boom.js"; export function getValue() { return value; }
// entry.js import { getValue } from "./consumer.js"; export default { fetch() { try { return new Response(String(getValue())); } catch (e) { return new Response("err: " + e.message); } }, };
Bundle:
esbuild entry.js --bundle --format=esm --target=es2022 --outfile=out.js
Run twice on a runtime that keeps the module cache (Workers, Deno Deploy, Lambda warm starts, a long-lived Node server). First call: real Error: init failed (the real cause). Second call: Cannot read properties of undefined (reading 'value') — the original error has been swallowed.
Why this hurts in practice
Amplified hugely on long-lived single-isolate runtimes. One isolate handles many requests, so a cold-start init failure produces one log line with the real error, then N log lines with the symptom error. Short log retention or reactive (rather than always-on) tailing means the real cause is unrecoverable without a redeploy.
OpenNext / @opennextjs/cloudflare users are particularly exposed because their middleware bundle wraps Next.js's _ENTRIES registry through __esm; an init error blanks the registry and every later request gets Cannot read properties of undefined (reading 'default').
Proposed fix
Capture the thrown error in a closure and re-throw on every subsequent call:
var __esm = (fn, res, err) => function __init() { if (err) throw err; if (fn) { try { res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0); } catch (e) { err = e; throw e; } } return res; };
One extra closed-over variable per wrapped module. Behavior change: a failed init becomes permanently sticky on the same module instance — which is what users almost always want (re-running an init that threw is rarely safe; side effects may have run).
Workaround
A post-build string-replace patch on the bundled output. Reference: https://github.com/teamvirtus/virtus-web/blob/main/scripts/patch-opennext-bundle.js
Happy to send a PR if you'd like.