Skip to content

module: throw error when re-runing errored module jobs #58957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const {
kEvaluating,
kEvaluationPhase,
kInstantiated,
kErrored,
kSourcePhase,
throwIfPromiseRejected,
} = internalBinding('module_wrap');
Expand Down Expand Up @@ -402,6 +403,9 @@ class ModuleLoader {
mod[kRequiredModuleSymbol] = job.module;
const { namespace } = job.runSync(parent);
return { wrap: job.module, namespace: namespace || job.module.getNamespace() };
} else if (status === kErrored) {
// If the module was previously imported and errored, throw the error.
throw job.module.getError();
}
// When the cached async job have already encountered a linking
// error that gets wrapped into a rejection, but is still later
Expand Down
10 changes: 8 additions & 2 deletions lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class ModuleJob extends ModuleJobBase {
assert(this.module instanceof ModuleWrap);
let status = this.module.getStatus();

debug('ModuleJob.runSync', this.module);
debug('ModuleJob.runSync()', status, this.module);
// FIXME(joyeecheung): this cannot fully handle < kInstantiated. Make the linking
// fully synchronous instead.
if (status === kUninstantiated) {
Expand Down Expand Up @@ -360,6 +360,7 @@ class ModuleJob extends ModuleJobBase {
}

async run(isEntryPoint = false) {
debug('ModuleJob.run()', this.module);
assert(this.phase === kEvaluationPhase);
await this.#instantiate();
if (isEntryPoint) {
Expand Down Expand Up @@ -465,7 +466,11 @@ class ModuleJobSync extends ModuleJobBase {
assert(this.phase === kEvaluationPhase);
// This path is hit by a require'd module that is imported again.
const status = this.module.getStatus();
if (status > kInstantiated) {
debug('ModuleJobSync.run()', status, this.module);
// If the module was previously required and errored, reject from import() again.
if (status === kErrored) {
throw this.module.getError();
} else if (status > kInstantiated) {
if (this.evaluationPromise) {
await this.evaluationPromise;
}
Expand All @@ -486,6 +491,7 @@ class ModuleJobSync extends ModuleJobBase {
}

runSync(parent) {
debug('ModuleJobSync.runSync()', this.module);
assert(this.phase === kEvaluationPhase);
// TODO(joyeecheung): add the error decoration logic from the async instantiate.
this.module.async = this.module.instantiateSync();
Expand Down
17 changes: 17 additions & 0 deletions test/es-module/test-import-module-retry-require-errored.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// This tests that after failing to import an ESM that rejects,
// retrying with require() still throws.

'use strict';
const common = require('../common');
const assert = require('assert');

(async () => {
await assert.rejects(import('../fixtures/es-modules/throw-error.mjs'), {
message: 'test',
});
Comment on lines +9 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it would be interesting to validate we catch the exact value being thrown, not just one with the same message.

assert.throws(() => {
require('../fixtures/es-modules/throw-error.mjs');
}, {
message: 'test',
});
})().catch(common.mustNotCall());
Copy link
Contributor

@aduh95 aduh95 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to catch never-settling promises (EDIT: I've opened #58992 to enforce that at the linter level, so it's fine to land it as is, I can update the other PR once this one has landed)

Suggested change
})().catch(common.mustNotCall());
})().then(common.mustCall());

Any reason not to use TLA/.mjs here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the promise never settles the test should just fail with exit code 13. On the other hand using catch with must not call would surface unwanted rejections.

If we use .mjs then we need to use createRequire() - I'd rather keep the plain require() since that's the more commonly hit path if some boilerplate is inevitable.

Copy link
Contributor

@aduh95 aduh95 Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the promise never settles the test should just fail with exit code 13

I think that's the case only for TLA, which we are not using here; you can convince yourself by adding a await new Promise(() => {}) at the top of the IIFE and see the test still passes.

16 changes: 16 additions & 0 deletions test/es-module/test-require-module-retry-import-errored-2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This tests that after failing to require an ESM that throws,
// retrying with import() still rejects.

'use strict';
const common = require('../common');
const assert = require('assert');

assert.throws(() => {
require('../fixtures/es-modules/throw-error.mjs');
}, {
message: 'test',
});

assert.rejects(import('../fixtures/es-modules/throw-error.mjs'), {
message: 'test',
}).catch(common.mustNotCall());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Suggested change
}).catch(common.mustNotCall());
}).then(common.mustCall());

Loading