Repository navigation
ERR_INVALID_RETURN_PROPERTY_VALUE when using module.register and module.registerHooks #57327
Description
Activity
It looks like a quirk in
module.register. If the hook looks like this:export async function load(url, context, nextLoad) { const result = await nextLoad(url, context); process._rawDebug('in async hook', result); return result; }
And it's the only hook used, it logs
in async hook [Object: null prototype] { format: 'commonjs', responseURL: 'file:///Users/joyee/projects/node/app.js', source: null }When loading CommonJS, the default load of the asynchronous hooks returns undefined as
source, which means technically the returned result of the default loading step of async hooks cannot be returned as-is, because that violates the API requirement that source is mandatory. It's either a caveat or a bug in the CommonJS loading inmodule.register.cc @nodejs/loaders
Sounds like it could be this caveat: https://nodejs.org/docs/latest/api/module.html#caveat-in-the-asynchronous-load-hook
The source is manually reset to null here
node/lib/internal/modules/esm/load.js
Lines 121 to 125 in b6df128
if (format === 'commonjs') { // For backward compatibility reasons, we need to discard the source in // order for the CJS loader to re-fetch it. source = null; } I feel that in essence this is similar to #55808 and the proper way to fix it might be to just revert the approach in #47999 and always use the CJS loader to handle customization hooks in require(). And to keep module.register working, just re-implement module.register as an internal synchronous hook using module.registerHooks. Then require() goes through the synchronous hooks from the CJS loader which contains that module.register wrapper. Then we no longer need to have this "reset source to null and load it twice" quirk.
Reacted by Timo Kössler, Hans Ott and Charles XuReacted by Hans OttReacted by Hans OttSounds like it could be this caveat
The docs seem to be talking about a different caveat (that is related to this one, or the cause of this one), because it seems to be talking about what happens when users return undefined in the asynchronous
loadhook. But the issue here is about the return value of the defaultnextLoadin the asyncrhonous hook. That means effectively an asynchornous load hook must always read the source from disk for CommonJS modules themselves. For example:export async function load(url, context, nextLoad) { const result = await nextLoad(url, context); const { source } = result; const processedSource = source + '\nconsole.log("altered")'; const final = { ...result, source: processedSource}; process._rawDebug(final); return final; }
Always logs this
{ format: 'commonjs', responseURL: 'file:///Users/joyee/projects/node/app.js', source: 'null\nconsole.log("altered")' } altered
The concatnated source code never contains the original code of the module and always starts with null if it's a CommonJS module. All the asynchronous load hook must have this branch or something similar to load source code for CommonJS themselves in case the next hook in chain is the default step:
export async function load(url, context, nextLoad) { const result = await nextLoad(url, context); if (!result.source && context.format === 'commonjs') { result.source = fs.readFileSync(fileURLToPath(url)); // Load it yourself } result.source; // Now it's safe to process source. }
For example, this is done by import-in-the-middle https://github.com/nodejs/import-in-the-middle/blob/53a33a9b07799bff815864089a0c072d223df47b/lib/get-exports.js#L100
Reacted by Timo Kössler- addedloadersIssues and PRs related to ES module loaders.Issues and PRs related to ES module loaders.
on Mar 6, 2025 FWIW, until the bug in
module.register()gets fixed, I think to work around the issue you are facing, you could either add a hook to the end of chain to catch that case formodule.register(), or ask the implementers of hooks that usemodule.register()to follow what import-in-the-middle does and be prepared thatnextLoaddoes not always return the source if they usemodule.register()(as the documentation says, returning a potentially null source can be unsupported in the future anyway).While testing the synchronous hooks with Sentry, I noticed that
import-in-the-middleis also affected by this issue.
The fix applied in nodejs/import-in-the-middle#202 solves this issue, but causes two other cache related exceptions to be thrown in certain cases. This occurs for example, ifrequire(esm)is used.node:internal/modules/esm/translators:152 return cjsCache.get(job.url).exports; ^ TypeError: Cannot read properties of undefined (reading 'exports')node:internal/modules/esm/module_job:325 this.module.async = this.module.instantiateSync(); ^ Error: request for 'file:///Users/timokoessler/Git/import-in-the-middle/lib/register.js' is not in cacheFor the full stack trace and reproduction, see timokoessler/nodejs-module-hooks-bug/tree/main/cache-issues.
cc. @joyeecheung
Reacted by Hans OttLooks like in the ESM loader there is a module format detection discrepancy:
ESM 26818: Translating StandardModule file:///Users/joyee/projects/nodejs-module-hooks-bug/cache-issues/sample-1/test.js ESM 26818: Storing file:///Users/joyee/projects/nodejs-module-hooks-bug/cache-issues/sample-1/test.js (implicit type) in ModuleLoadMap ESM 26818: ModuleJob.runSync() 0 ModuleWrap { sourceURL: undefined, sourceMapURL: undefined, url: 'file:///Users/joyee/projects/nodejs-module-hooks-bug/cache-issues/sample-1/test.js' } test node:internal/modules/esm/translators:153 return cjsCache.get(job.url).exports; ^ TypeError: Cannot read properties of undefined (reading 'exports')It was detected as ESM by one path and then executed as CJS by another (this file only contains a
console.log()so it could be both).Reacted by Timo KösslerLooked into it a bit, I verified #59679 is enough to fix https://github.com/timokoessler/nodejs-module-hooks-bug/tree/main/cache-issues - I haven't checked the other one with import-in-the-middle PR, I suspect it would be fixed by that too.
Reacted by Timo Kössler and Hans OttReacted by Jacob SmithReacted by Timo Kössler and Hans Ott- added a commit that references this issue
on Oct 14, 2025 - added a commit that references this issue
on Oct 23, 2025 - added 2 commits that reference this issue
on Oct 31, 2025 - added a commit that references this issue
on Apr 27, 2026 - added a commit that references this issue
on Jul 22, 2026 - added a commit that references this issue
on Oct 9, 2026
Version
v23.9.0
Platform
Subsystem
No response
What steps will reproduce the bug?
Create the following files:
instrument.js
hooks.js
app.js
Run
node --import ./instrument.js ./app.jsHow often does it reproduce? Is there a required condition?
Always, if the project includes a CJS file.
What is the expected behavior? Why is that the expected behavior?
No exception, logs
Hello from app.jsWhat do you see instead?
Additional information
Real world cases where this bug occurs
module.register(e.g. tapjs) with an app that usesmodule.registerHooksmodule.register(e.g. Sentry) and the other usingmodule.registerHooksMore information
package.jsonwith"type:" "module"and re-run the command above.Git Repo: timokoessler/nodejs-module-hooks-bug