Running an OpenTelemetry-instrumented ESM app under Nub crashes at startup. Plain Node with the same loader works.
Repro
npm init -y && npm pkg set type=module
npm i ms @opentelemetry/instrumentation
printf 'import ms from "ms";\nconsole.log("APP_OK");\n' > app.mjs
L=node_modules/@opentelemetry/instrumentation/hook.mjs
node --experimental-loader=$L app.mjs # exit 0, APP_OK
nub app.mjs # exit 0, APP_OK
NODE_OPTIONS="--experimental-loader=$L" nub app.mjs # exit 1
TypeError [ERR_INVALID_RETURN_PROPERTY_VALUE]: Expected a string, an ArrayBuffer, or a TypedArray to be returned for the "source" from the "load" hook but got null.
at validateSourceStrict (node:internal/modules/customization_hooks:293:11)
at validateLoadSloppy (node:internal/modules/customization_hooks:278:3)
at #loadSync (node:internal/modules/esm/loader:832:16)
Nub 0.6.0, Node 26.5.0, macOS arm64.
Cause
The recovery path at runtime/preload-common.cjs:649-672 handles the loadSync stub Node throws when a user async module.register loader is present — which is what OpenTelemetry's ESM hook is. Its builtin branch returns:
return { format: "builtin", source: null, shortCircuit: true };
Node's validateSourceStrict accepts a null source only when format === "commonjs". Line 541 does that legally; the builtin branch does not, so the recovery path itself throws.
Ruled out
- The version band in
nodeHookComposeBroken() (22.15.0–24.11.0) leaves sync hooks installed on Node 26 rather than downgrading. Not the cause: a plain-Node sync registerHooks pass-through composed with the same OpenTelemetry async loader on Node 26.5.0 exits 0.
- Setting
__NUB_FORCE_ASYNC_TIER=1 is not a workaround, and cannot be tested that way — Nub strips the variable before the child sees it, so the child reads undefined. It is launcher-set only, via force_async_tier_env in spawn.rs. Crash traces with and without it are byte-identical.
Impact
Any ESM project using OpenTelemetry auto-instrumentation. The ESM hook is the only supported way to instrument ESM imports — the --require/--import register entrypoint alone registers no ESM loader — so this is the standard path, not an exotic configuration. No known workaround.
Running an OpenTelemetry-instrumented ESM app under Nub crashes at startup. Plain Node with the same loader works.
Repro
Nub 0.6.0, Node 26.5.0, macOS arm64.
Cause
The recovery path at
runtime/preload-common.cjs:649-672handles theloadSyncstub Node throws when a user asyncmodule.registerloader is present — which is what OpenTelemetry's ESM hook is. Its builtin branch returns:Node's
validateSourceStrictaccepts a nullsourceonly whenformat === "commonjs". Line 541 does that legally; the builtin branch does not, so the recovery path itself throws.Ruled out
nodeHookComposeBroken()(22.15.0–24.11.0) leaves sync hooks installed on Node 26 rather than downgrading. Not the cause: a plain-Node syncregisterHookspass-through composed with the same OpenTelemetry async loader on Node 26.5.0 exits 0.__NUB_FORCE_ASYNC_TIER=1is not a workaround, and cannot be tested that way — Nub strips the variable before the child sees it, so the child readsundefined. It is launcher-set only, viaforce_async_tier_envinspawn.rs. Crash traces with and without it are byte-identical.Impact
Any ESM project using OpenTelemetry auto-instrumentation. The ESM hook is the only supported way to instrument ESM imports — the
--require/--importregister entrypoint alone registers no ESM loader — so this is the standard path, not an exotic configuration. No known workaround.