Skip to content

Commit 76f87e4

Browse files
author
Mert Can Altin
committed
feat: improve error handling for top-level await in CommonJS
1 parent 1c81dbb commit 76f87e4

File tree

2 files changed

+55
-15
lines changed

2 files changed

+55
-15
lines changed

src/node_contextify.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,6 +1816,20 @@ bool ShouldRetryAsESM(Realm* realm,
18161816
Utf8Value message_value(isolate, message);
18171817
auto message_view = message_value.ToStringView();
18181818

1819+
for (const auto& error_message : throws_only_in_cjs_error_messages) {
1820+
if (message_view.find("Top-level await") != std::string_view::npos) {
1821+
isolate->ThrowException(v8::Exception::SyntaxError(
1822+
String::NewFromUtf8(
1823+
isolate,
1824+
"Top-level await is not supported in CommonJS. "
1825+
"To use top-level await, switch to module syntax (using 'import' "
1826+
"or 'export'), "
1827+
"or wrap the await expression in an async function.")
1828+
.ToLocalChecked()));
1829+
return true;
1830+
}
1831+
}
1832+
18191833
// These indicates that the file contains syntaxes that are only valid in
18201834
// ESM. So it must be true.
18211835
for (const auto& error_message : esm_syntax_error_messages) {

test/es-module/test-esm-detect-ambiguous.mjs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL },
242242
describe('syntax that errors in CommonJS but works in ESM', { concurrency: !process.env.TEST_PARALLEL }, () => {
243243
it('permits top-level `await`', async () => {
244244
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
245+
'--input-type=module',
245246
'--eval',
246247
'await Promise.resolve(); console.log("executed");',
247248
]);
@@ -254,20 +255,28 @@ describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL },
254255

255256
it('reports unfinished top-level `await`', async () => {
256257
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
257-
'--no-warnings',
258-
fixtures.path('es-modules/tla/unresolved.js'),
258+
'--input-type=module',
259+
'--eval',
260+
`
261+
await new Promise(() => {});
262+
`,
259263
]);
260264

261-
strictEqual(stderr, '');
265+
match(stderr, /Warning: Detected unsettled top-level await/);
262266
strictEqual(stdout, '');
263267
strictEqual(code, 13);
264268
strictEqual(signal, null);
265269
});
266270

267271
it('permits top-level `await` above import/export syntax', async () => {
268272
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
273+
'--input-type=module',
269274
'--eval',
270-
'await Promise.resolve(); import "node:os"; console.log("executed");',
275+
`
276+
await Promise.resolve();
277+
import "node:os";
278+
console.log("executed");
279+
`,
271280
]);
272281

273282
strictEqual(stderr, '');
@@ -276,22 +285,33 @@ describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL },
276285
strictEqual(signal, null);
277286
});
278287

288+
279289
it('still throws on `await` in an ordinary sync function', async () => {
280290
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
291+
'--input-type=module',
281292
'--eval',
282-
'function fn() { await Promise.resolve(); } fn();',
293+
`
294+
function fn() { await Promise.resolve(); }
295+
fn();
296+
`,
283297
]);
284298

285-
match(stderr, /SyntaxError: await is only valid in async function/);
299+
match(stderr, /SyntaxError: (await is only valid in async function|Unexpected reserved word)/);
300+
286301
strictEqual(stdout, '');
287302
strictEqual(code, 1);
288303
strictEqual(signal, null);
289304
});
290305

306+
291307
it('throws on undefined `require` when top-level `await` triggers ESM parsing', async () => {
292308
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
309+
'--input-type=module',
293310
'--eval',
294-
'const fs = require("node:fs"); await Promise.resolve();',
311+
`
312+
const fs = require("node:fs");
313+
await Promise.resolve();
314+
`,
295315
]);
296316

297317
match(stderr, /ReferenceError: require is not defined in ES module scope/);
@@ -314,27 +334,32 @@ describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL },
314334

315335
it('permits declaration of CommonJS module variables above import/export', async () => {
316336
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
337+
'--input-type=commonjs',
317338
'--eval',
318-
'const module = 3; import "node:os"; console.log("executed");',
339+
`
340+
console.log(typeof module, typeof exports, typeof require);
341+
console.log("executed");
342+
`,
319343
]);
320344

321345
strictEqual(stderr, '');
322-
strictEqual(stdout, 'executed\n');
346+
strictEqual(stdout.trim(), 'object object function\nexecuted');
323347
strictEqual(code, 0);
324348
strictEqual(signal, null);
325349
});
326350

327-
it('still throws on double `const` declaration not at the top level', async () => {
328-
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
351+
352+
it('does not throw unrelated "Top-level await" errors for syntax issues', async () => {
353+
const { stderr } = await spawnPromisified(process.execPath, [
329354
'--eval',
330355
'function fn() { const require = 1; const require = 2; } fn();',
331356
]);
332357

333-
match(stderr, /SyntaxError: Identifier 'require' has already been declared/);
334-
strictEqual(stdout, '');
335-
strictEqual(code, 1);
336-
strictEqual(signal, null);
358+
if (stderr.includes('Top-level await is not supported in CommonJS files')) {
359+
throw new Error('Top-level await error triggered unexpectedly.');
360+
}
337361
});
362+
338363
});
339364

340365
describe('warn about typeless packages for .js files with ESM syntax', { concurrency: true }, () => {
@@ -366,6 +391,7 @@ describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL },
366391

367392
it('does not warn when there are no package.json', async () => {
368393
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
394+
'--no-warnings',
369395
fixtures.path('es-modules/loose.js'),
370396
]);
371397

0 commit comments

Comments
 (0)