Version
v26.5.1 (behavior present from v22.x through main; see the source reference below)
Platform
Reproduced on darwin arm64; the code path is platform-independent.
Subsystem
module, esm
What steps will reproduce the bug?
mkdir -p repro/node_modules/dep/lib && cd repro
printf '{"name":"dep","exports":{".":"./lib/real.js","./sub":"./lib/sub.js"}}' > node_modules/dep/package.json
printf 'export const which = "real";' > node_modules/dep/lib/real.js
printf 'export const which = "sub";' > node_modules/dep/lib/sub.js
printf 'export const which = "decoy index.js";' > node_modules/dep/index.js
printf '{"type":"module"}' > package.json
cat > main.mjs <<'EOF'
const sub = await import("dep/sub").then(m => m.which, e => `${e.code}`);
const root = await import("dep").then(m => m.which, e => `${e.code}`);
console.log("dep/sub ->", sub);
console.log("dep ->", root);
EOF
node main.mjs
# dep/sub -> sub
# dep -> real
chmod 000 node_modules/dep/package.json # any read failure; EACCES stands in for EMFILE/EIO/ENOMEM
node main.mjs
# dep/sub -> ERR_MODULE_NOT_FOUND
# dep -> decoy index.js
What is the expected behavior?
A package.json that exists but cannot be read should surface the read error (or at minimum fail resolution with an error that names the errno), the way an unparsable package.json raises ERR_INVALID_PACKAGE_CONFIG.
What do you see instead?
The read failure is treated identically to the file being absent:
import("dep/sub") fails ERR_MODULE_NOT_FOUND for a subpath the package exports — with a Did you mean to import "dep/lib/sub.js"?-style hint in real packages, pointing users away from the actual problem.
import("dep") silently executes a different file than the package declares — legacy index.js resolution instead of the exports target. No error, no warning; wrong code runs.
The swallow is in src/node_modules.cc (GetPackageJSON):
if (ReadFileSync(&package_config.raw_json, path.data()) < 0) {
return nullptr;
}
ReadFileSync's failure reason is discarded, so ENOENT (genuinely absent — the reasonable case to treat as "no package.json") is indistinguishable from EACCES, EMFILE, EIO, or ENOMEM on a file that exists.
Additional information
We hit this in CI as an unexplainable one-off: a correctly installed package with a correct exports map failed a subpath import with ERR_MODULE_NOT_FOUND on a heavily loaded runner, and everything on disk was byte-identical before and after. The root cause took a while to establish precisely because the errno is discarded — a transient read failure leaves no trace and masquerades as a missing module (taras/executable.md#439 has the investigation). Distinguishing ENOENT from other errnos at this one spot would turn silent misresolution into a diagnosable error.
Related but distinct: #54304 (over-long path makes the read fail, module type misdetected — same swallow surfacing through a different trigger), #49674 (misleading message when package.json exists but defines no matching export).
Version
v26.5.1 (behavior present from v22.x through main; see the source reference below)
Platform
Reproduced on darwin arm64; the code path is platform-independent.
Subsystem
module, esm
What steps will reproduce the bug?
What is the expected behavior?
A
package.jsonthat exists but cannot be read should surface the read error (or at minimum fail resolution with an error that names the errno), the way an unparsablepackage.jsonraisesERR_INVALID_PACKAGE_CONFIG.What do you see instead?
The read failure is treated identically to the file being absent:
import("dep/sub")failsERR_MODULE_NOT_FOUNDfor a subpath the package exports — with aDid you mean to import "dep/lib/sub.js"?-style hint in real packages, pointing users away from the actual problem.import("dep")silently executes a different file than the package declares — legacyindex.jsresolution instead of theexportstarget. No error, no warning; wrong code runs.The swallow is in
src/node_modules.cc(GetPackageJSON):ReadFileSync's failure reason is discarded, so ENOENT (genuinely absent — the reasonable case to treat as "no package.json") is indistinguishable from EACCES, EMFILE, EIO, or ENOMEM on a file that exists.Additional information
We hit this in CI as an unexplainable one-off: a correctly installed package with a correct
exportsmap failed a subpath import withERR_MODULE_NOT_FOUNDon a heavily loaded runner, and everything on disk was byte-identical before and after. The root cause took a while to establish precisely because the errno is discarded — a transient read failure leaves no trace and masquerades as a missing module (taras/executable.md#439 has the investigation). Distinguishing ENOENT from other errnos at this one spot would turn silent misresolution into a diagnosable error.Related but distinct: #54304 (over-long path makes the read fail, module type misdetected — same swallow surfacing through a different trigger), #49674 (misleading message when
package.jsonexists but defines no matching export).