You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Cannot read properties of undefined when requiring node:fs with hooks #60005
How often does it reproduce? Is there a required condition?
Happens reliably when fs is being required as node:fs, does not happen when required as fs, or when imported.
What is the expected behavior? Why is that the expected behavior?
Should either require fs-mock.mjs, or require original (builtin) fs module.
What do you see instead?
node:internal/modules/cjs/loader:1155
return mod.exports;
^
TypeError: Cannot read properties of undefined (reading 'exports')
at loadBuiltinWithHooks (node:internal/modules/cjs/loader:1155:14)
at Function._load (node:internal/modules/cjs/loader:1197:20)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:237:24)
at Module.require (node:internal/modules/cjs/loader:1463:12)
at require (node:internal/modules/helpers:147:16)
at Object.<anonymous> (/vagrant/index.cjs:1:12)
at Module._compile (node:internal/modules/cjs/loader:1706:14)
at Object..js (node:internal/modules/cjs/loader:1839:10)
at Module.load (node:internal/modules/cjs/loader:1441:32)
Additional information
Output from NODE_DEBUG=* node --import=./register.mjs index.cjs: log.txt
Thanks for the suggestion, but I don't think that's it:
It works when I replace require('node:fs') with require('fs'), without changing the contents of fs-mock.mjs. It evens works when I directly require('mock-fs'), or when I require('dummy') and change the resolution logic accordingly.
When I add a throw statement at the top of fs-mock.mjs, no error is thrown, which would seem to indicate that fs-mock.mjs never even gets loaded. Update: Just to make extra sure, I revoked all read access to fs-mock.mjs, but there is no EACCES. This confirms the module never gets loaded at all.
I know that Node's module resolution and loading code takes a different path for loading core modules when using require. I think this bug has to do with that.
I tried debugging Node.js core and observed the same behavior you’re discussing—not only for "node:fs", but for any specifier that starts with "node:". Even a custom specifier like "node:scoheart" hits the same path.
Concretely, every "node:" request enters the conditional StringPrototypeStartsWith(request, 'node:'), then proceeds into loadBuiltinWithHooks, and eventually attempts to load via loadBuiltinModule. The relevant source looks like this:
// node/lib/internal/modules/cjs/loader.jsfunctionloadBuiltinWithHooks(id,url,format){if(loadHooks.length){url??=`node:${id}`;// TODO(joyeecheung): do we really want to invoke the load hook for the builtins?constloadResult=loadWithHooks(url,format||'builtin',/* importAttributes */undefined,getCjsConditionsArray(),getDefaultLoad(url,id));if(loadResult.format&&loadResult.format!=='builtin'){returnundefined;// Format has been overridden, return undefined for the caller to continue loading.}}// No hooks or the hooks have not overridden the format. Load it as a builtin module and return the exports.constmod=loadBuiltinModule(id);returnmod.exports;}Module._load=function(request,parent,isMain){
...
const{ url, format, filename }=resolveForCJSWithHooks(request,parent,isMain);// For backwards compatibility, if the request itself starts with node:, load it before checking// Module._cache. Otherwise, load it after the check.if(StringPrototypeStartsWith(request,'node:')){constresult=loadBuiltinWithHooks(filename,url,format);if(result){returnresult;}// The format of the builtin has been overridden by user hooks. Continue loading.}constcachedModule=Module._cache[filename];
...
}
What I see at runtime:
request === "node:fs" with no hooks registered, resolveForCJSWithHooks returns:
{url: "node:fs",format: "builtin",filename: "fs"}
This leads to loadBuiltinModule("fs"), which returns the built-in fs module as expected.
request === "node:fs" with custom hooks registered, resolveForCJSWithHooks returns:
Because the original request still starts with "node:", it still enters the if (StringPrototypeStartsWith(request, 'node:')) branch and calls:
loadBuiltinWithHooks(filename,// id === "/Users/.../fsmock.mjs"url,// "file:///Users/.../fsmock.mjs"format// undefined)loadBuiltinModule(id)// here id is the filename path —— "/Users/.../fsmock.mjs"
Since id is now an absolute path (not a known builtin like "fs"), loadBuiltinModule("/Users/.../fsmock.mjs") fails—there is obviously no builtin module by that name—leading to the error I’m seeing.
I’m not entirely sure whether this should be considered a bug or simply the intended behavior of the "node:" scheme plus the “backwards compatibility” branch in _load.
Question to maintainers
Is this the expected, intentional behavior for "node:" specifiers (including custom ones), or should custom "node:" specifiers be treated differently so they don’t fall through to loadBuiltinModule unless they resolve to an actual builtin?
Activity
timfish commentedon Oct 2, 2025
You are mocking the fs module in ESM but the original is CommonJs
BadIdeaException commentedon Oct 4, 2025
Thanks for the suggestion, but I don't think that's it:
require('node:fs')withrequire('fs'), without changing the contents offs-mock.mjs. It evens works when I directlyrequire('mock-fs'), or when Irequire('dummy')and change the resolution logic accordingly.throwstatement at the top offs-mock.mjs, no error is thrown, which would seem to indicate thatfs-mock.mjsnever even gets loaded. Update: Just to make extra sure, I revoked all read access tofs-mock.mjs, but there is noEACCES. This confirms the module never gets loaded at all.I know that Node's module resolution and loading code takes a different path for loading core modules when using
require. I think this bug has to do with that.Scoheart commentedon Oct 5, 2025
I tried debugging Node.js core and observed the same behavior you’re discussing—not only for "node:fs", but for any specifier that starts with "node:". Even a custom specifier like "node:scoheart" hits the same path.
Concretely, every "node:" request enters the conditional StringPrototypeStartsWith(request, 'node:'), then proceeds into loadBuiltinWithHooks, and eventually attempts to load via loadBuiltinModule. The relevant source looks like this:
What I see at runtime:
This leads to loadBuiltinModule("fs"), which returns the built-in fs module as expected.
Because the original request still starts with "node:", it still enters the if (StringPrototypeStartsWith(request, 'node:')) branch and calls:
Since id is now an absolute path (not a known builtin like "fs"), loadBuiltinModule("/Users/.../fsmock.mjs") fails—there is obviously no builtin module by that name—leading to the error I’m seeing.
I’m not entirely sure whether this should be considered a bug or simply the intended behavior of the "node:" scheme plus the “backwards compatibility” branch in _load.
Question to maintainers
Is this the expected, intentional behavior for "node:" specifiers (including custom ones), or should custom "node:" specifiers be treated differently so they don’t fall through to loadBuiltinModule unless they resolve to an actual builtin?
joyeecheung commentedon Dec 6, 2025
I think in this case, it should use the overriden resolution result as expected. I have a fix in https://github.com/joyeecheung/node/tree/fix-node-hooks though I will need to further test it with the load hooks before sending a PR.
BadIdeaException commentedon Dec 15, 2025
Nice 👍 I hope that gets merged soon, then.
module: fix sync resolve hooks for require with node: prefixes
module: fix sync resolve hooks for require with node: prefixes
module: fix sync resolve hooks for require with node: prefixes
module: fix sync resolve hooks for require with node: prefixes
module: fix sync resolve hooks for require with node: prefixes
BadIdeaException commentedon Jan 26, 2026
@joyeecheung Has this landed yet? With node 25.3.0 I'm still seeing the same problem...
joyeecheung commentedon Jan 26, 2026
The fix is out in 25.4.0 so you will need to upgrade to test it.
BadIdeaException commentedon Feb 8, 2026
It works now. Thank you for taking care of this so quickly. 👍 +1 karma points from me. 😌
joyeecheung commentedon Feb 8, 2026
Thanks, karma is what I am after!
chore: bump node.js minimum version to 25.4
module: fix sync resolve hooks for require with node: prefixes