Skip to content

Commit 8d10bc7

Browse files
joyeecheungnodejs-github-bot
authored andcommitted
module: improve error message from asynchronicity in require(esm)
- Improve the error message that shows up when there is a race from doing require(esm) and import(esm) at the same time. - Improve error message of ERR_REQUIRE_ASYNC_MODULE by showing parent and target file names, if available. Drive-by: split the require(tla) tests since we are modifying the tests already. PR-URL: #57126 Refs: https://github.com/fisker/prettier-issue-17139 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]>
1 parent d601c7d commit 8d10bc7

13 files changed

+189
-78
lines changed

lib/internal/errors.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -1656,9 +1656,18 @@ E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error);
16561656
E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error);
16571657
E('ERR_QUIC_TRANSPORT_ERROR', 'A QUIC transport error occurred. %d [%s]', Error);
16581658
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR', 'The QUIC session requires version negotiation', Error);
1659-
E('ERR_REQUIRE_ASYNC_MODULE', 'require() cannot be used on an ESM ' +
1659+
E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parentFilename) {
1660+
let message = 'require() cannot be used on an ESM ' +
16601661
'graph with top-level await. Use import() instead. To see where the' +
1661-
' top-level await comes from, use --experimental-print-required-tla.', Error);
1662+
' top-level await comes from, use --experimental-print-required-tla.';
1663+
if (parentFilename) {
1664+
message += `\n From ${parentFilename} `;
1665+
}
1666+
if (filename) {
1667+
message += `\n Requiring ${filename} `;
1668+
}
1669+
return message;
1670+
}, Error);
16621671
E('ERR_REQUIRE_CYCLE_MODULE', '%s', Error);
16631672
E('ERR_REQUIRE_ESM',
16641673
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {

lib/internal/modules/esm/loader.js

+21-4
Original file line numberDiff line numberDiff line change
@@ -338,20 +338,37 @@ class ModuleLoader {
338338
// TODO(joyeecheung): ensure that imported synchronous graphs are evaluated
339339
// synchronously so that any previously imported synchronous graph is already
340340
// evaluated at this point.
341+
// TODO(joyeecheung): add something similar to CJS loader's requireStack to help
342+
// debugging the the problematic links in the graph for import.
341343
if (job !== undefined) {
342344
mod[kRequiredModuleSymbol] = job.module;
345+
const parentFilename = urlToFilename(parent?.filename);
346+
// TODO(node:55782): this race may stop to happen when the ESM resolution and loading become synchronous.
347+
if (!job.module) {
348+
let message = `Cannot require() ES Module ${filename} because it is not yet fully loaded. `;
349+
message += 'This may be caused by a race condition if the module is simultaneously dynamically ';
350+
message += 'import()-ed via Promise.all(). Try await-ing the import() sequentially in a loop instead.';
351+
if (parentFilename) {
352+
message += ` (from ${parentFilename})`;
353+
}
354+
assert(job.module, message);
355+
}
343356
if (job.module.async) {
344-
throw new ERR_REQUIRE_ASYNC_MODULE();
357+
throw new ERR_REQUIRE_ASYNC_MODULE(filename, parentFilename);
345358
}
359+
// job.module may be undefined if it's asynchronously loaded. Which means
360+
// there is likely a cycle.
346361
if (job.module.getStatus() !== kEvaluated) {
347-
const parentFilename = urlToFilename(parent?.filename);
348362
let message = `Cannot require() ES Module ${filename} in a cycle.`;
349363
if (parentFilename) {
350364
message += ` (from ${parentFilename})`;
351365
}
366+
message += 'A cycle involving require(esm) is disallowed to maintain ';
367+
message += 'invariants madated by the ECMAScript specification';
368+
message += 'Try making at least part of the dependency in the graph lazily loaded.';
352369
throw new ERR_REQUIRE_CYCLE_MODULE(message);
353370
}
354-
return { wrap: job.module, namespace: job.module.getNamespaceSync() };
371+
return { wrap: job.module, namespace: job.module.getNamespaceSync(filename, parentFilename) };
355372
}
356373
// TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the
357374
// cache here, or use a carrier object to carry the compiled module script
@@ -363,7 +380,7 @@ class ModuleLoader {
363380
job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk);
364381
this.loadCache.set(url, kImplicitTypeAttribute, job);
365382
mod[kRequiredModuleSymbol] = job.module;
366-
return { wrap: job.module, namespace: job.runSync().namespace };
383+
return { wrap: job.module, namespace: job.runSync(parent).namespace };
367384
}
368385

369386
/**

lib/internal/modules/esm/module_job.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const assert = require('internal/assert');
3636
const resolvedPromise = PromiseResolve();
3737
const {
3838
setHasStartedUserESMExecution,
39+
urlToFilename,
3940
} = require('internal/modules/helpers');
4041
const { getOptionValue } = require('internal/options');
4142
const noop = FunctionPrototype;
@@ -380,7 +381,7 @@ class ModuleJobSync extends ModuleJobBase {
380381
`Status = ${status}`);
381382
}
382383

383-
runSync() {
384+
runSync(parent) {
384385
// TODO(joyeecheung): add the error decoration logic from the async instantiate.
385386
this.module.async = this.module.instantiateSync();
386387
// If --experimental-print-required-tla is true, proceeds to evaluation even
@@ -389,11 +390,13 @@ class ModuleJobSync extends ModuleJobBase {
389390
// TODO(joyeecheung): track the asynchroniticy using v8::Module::HasTopLevelAwait()
390391
// and we'll be able to throw right after compilation of the modules, using acron
391392
// to find and print the TLA.
393+
const parentFilename = urlToFilename(parent?.filename);
394+
const filename = urlToFilename(this.url);
392395
if (this.module.async && !getOptionValue('--experimental-print-required-tla')) {
393-
throw new ERR_REQUIRE_ASYNC_MODULE();
396+
throw new ERR_REQUIRE_ASYNC_MODULE(filename, parentFilename);
394397
}
395398
setHasStartedUserESMExecution();
396-
const namespace = this.module.evaluateSync();
399+
const namespace = this.module.evaluateSync(filename, parentFilename);
397400
return { __proto__: null, module: this.module, namespace };
398401
}
399402
}

src/module_wrap.cc

+2-2
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ void ModuleWrap::EvaluateSync(const FunctionCallbackInfo<Value>& args) {
710710
FPrintF(stderr, "%s\n", reason);
711711
}
712712
}
713-
THROW_ERR_REQUIRE_ASYNC_MODULE(env);
713+
THROW_ERR_REQUIRE_ASYNC_MODULE(env, args[0], args[1]);
714714
return;
715715
}
716716

@@ -740,7 +740,7 @@ void ModuleWrap::GetNamespaceSync(const FunctionCallbackInfo<Value>& args) {
740740
}
741741

742742
if (module->IsGraphAsync()) {
743-
return THROW_ERR_REQUIRE_ASYNC_MODULE(realm->env());
743+
return THROW_ERR_REQUIRE_ASYNC_MODULE(realm->env(), args[0], args[1]);
744744
}
745745
Local<Value> result = module->GetModuleNamespace();
746746
args.GetReturnValue().Set(result);

src/node_errors.h

+22-4
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,6 @@ ERRORS_WITH_CODE(V)
215215
"creating Workers") \
216216
V(ERR_NON_CONTEXT_AWARE_DISABLED, \
217217
"Loading non context-aware native addons has been disabled") \
218-
V(ERR_REQUIRE_ASYNC_MODULE, \
219-
"require() cannot be used on an ESM graph with top-level await. Use " \
220-
"import() instead. To see where the top-level await comes from, use " \
221-
"--experimental-print-required-tla.") \
222218
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, \
223219
"Script execution was interrupted by `SIGINT`") \
224220
V(ERR_TLS_PSK_SET_IDENTITY_HINT_FAILED, "Failed to set PSK identity hint") \
@@ -248,6 +244,28 @@ inline void THROW_ERR_SCRIPT_EXECUTION_TIMEOUT(Environment* env,
248244
THROW_ERR_SCRIPT_EXECUTION_TIMEOUT(env, message.str().c_str());
249245
}
250246

247+
inline void THROW_ERR_REQUIRE_ASYNC_MODULE(
248+
Environment* env,
249+
v8::Local<v8::Value> filename,
250+
v8::Local<v8::Value> parent_filename) {
251+
static constexpr const char* prefix =
252+
"require() cannot be used on an ESM graph with top-level await. Use "
253+
"import() instead. To see where the top-level await comes from, use "
254+
"--experimental-print-required-tla.";
255+
std::string message = prefix;
256+
if (!parent_filename.IsEmpty() && parent_filename->IsString()) {
257+
Utf8Value utf8(env->isolate(), parent_filename);
258+
message += "\n From ";
259+
message += utf8.out();
260+
}
261+
if (!filename.IsEmpty() && filename->IsString()) {
262+
Utf8Value utf8(env->isolate(), filename);
263+
message += "\n Requiring ";
264+
message += +utf8.out();
265+
}
266+
THROW_ERR_REQUIRE_ASYNC_MODULE(env, message.c_str());
267+
}
268+
251269
inline v8::Local<v8::Object> ERR_BUFFER_TOO_LARGE(v8::Isolate* isolate) {
252270
char message[128];
253271
snprintf(message,

test/common/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,17 @@ function expectRequiredModule(mod, expectation, checkESModule = true) {
855855
assert.deepStrictEqual(clone, { ...expectation });
856856
}
857857

858+
function expectRequiredTLAError(err) {
859+
const message = /require\(\) cannot be used on an ESM graph with top-level await/;
860+
if (typeof err === 'string') {
861+
assert.match(err, /ERR_REQUIRE_ASYNC_MODULE/);
862+
assert.match(err, message);
863+
} else {
864+
assert.strictEqual(err.code, 'ERR_REQUIRE_ASYNC_MODULE');
865+
assert.match(err.message, message);
866+
}
867+
}
868+
858869
const common = {
859870
allowGlobals,
860871
buildType,
@@ -864,6 +875,7 @@ const common = {
864875
escapePOSIXShell,
865876
expectsError,
866877
expectRequiredModule,
878+
expectRequiredTLAError,
867879
expectWarning,
868880
getArrayBufferViews,
869881
getBufferSources,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict';
2+
3+
// Tests that require(esm) with top-level-await throws before execution starts
4+
// if --experimental-print-required-tla is not enabled.
5+
6+
const common = require('../common');
7+
const assert = require('assert');
8+
const { spawnSyncAndExit } = require('../common/child_process');
9+
const fixtures = require('../common/fixtures');
10+
11+
{
12+
spawnSyncAndExit(process.execPath, [
13+
fixtures.path('es-modules/tla/require-execution.js'),
14+
], {
15+
signal: null,
16+
status: 1,
17+
stderr(output) {
18+
assert.doesNotMatch(output, /I am executed/);
19+
common.expectRequiredTLAError(output);
20+
assert.match(output, /From .*require-execution\.js/);
21+
assert.match(output, /Requiring .*execution\.mjs/);
22+
return true;
23+
},
24+
stdout: ''
25+
});
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
// Tests that require(esm) throws for top-level-await in inner graphs.
4+
5+
const common = require('../common');
6+
const assert = require('assert');
7+
8+
assert.throws(() => {
9+
require('../fixtures/es-modules/tla/parent.mjs');
10+
}, (err) => {
11+
common.expectRequiredTLAError(err);
12+
assert.match(err.message, /From .*test-require-module-tla-nested\.js/);
13+
assert.match(err.message, /Requiring .*parent\.mjs/);
14+
return true;
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
// Tests that require(esm) with top-level-await throws after execution
4+
// if --experimental-print-required-tla is enabled.
5+
6+
const common = require('../common');
7+
const assert = require('assert');
8+
const { spawnSyncAndExit } = require('../common/child_process');
9+
const fixtures = require('../common/fixtures');
10+
11+
{
12+
spawnSyncAndExit(process.execPath, [
13+
'--experimental-print-required-tla',
14+
fixtures.path('es-modules/tla/require-execution.js'),
15+
], {
16+
signal: null,
17+
status: 1,
18+
stderr(output) {
19+
assert.match(output, /I am executed/);
20+
common.expectRequiredTLAError(output);
21+
assert.match(output, /Error: unexpected top-level await at.*execution\.mjs:3/);
22+
assert.match(output, /await Promise\.resolve\('hi'\)/);
23+
assert.match(output, /From .*require-execution\.js/);
24+
assert.match(output, /Requiring .*execution\.mjs/);
25+
return true;
26+
},
27+
stdout: ''
28+
});
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
// Tests that require(esm) throws for rejected top-level await.
4+
5+
const common = require('../common');
6+
const assert = require('assert');
7+
8+
assert.throws(() => {
9+
require('../fixtures/es-modules/tla/rejected.mjs');
10+
}, (err) => {
11+
common.expectRequiredTLAError(err);
12+
assert.match(err.message, /From .*test-require-module-tla-rejected\.js/);
13+
assert.match(err.message, /Requiring .*rejected\.mjs/);
14+
return true;
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
// Tests that require(esm) throws for resolved top-level-await.
4+
5+
const common = require('../common');
6+
const assert = require('assert');
7+
8+
assert.throws(() => {
9+
require('../fixtures/es-modules/tla/resolved.mjs');
10+
}, (err) => {
11+
common.expectRequiredTLAError(err);
12+
assert.match(err.message, /From .*test-require-module-tla-resolved\.js/);
13+
assert.match(err.message, /Requiring .*resolved\.mjs/);
14+
return true;
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
// Tests that require(esm) throws for unresolved top-level-await.
4+
5+
const common = require('../common');
6+
const assert = require('assert');
7+
8+
assert.throws(() => {
9+
require('../fixtures/es-modules/tla/unresolved.mjs');
10+
}, (err) => {
11+
common.expectRequiredTLAError(err);
12+
assert.match(err.message, /From .*test-require-module-tla-unresolved\.js/);
13+
assert.match(err.message, /Requiring .*unresolved\.mjs/);
14+
return true;
15+
});

test/es-module/test-require-module-tla.js

-63
This file was deleted.

0 commit comments

Comments
 (0)