Skip to content

Commit 35ef990

Browse files
committed
module: handle Top-Level Await non-fulfills better
Handle situations in which the main `Promise` from a TLA module is not fulfilled better: - When not resolving the `Promise` at all, set a non-zero exit code (unless another one has been requested explicitly) to distinguish the result from a successful completion. - When rejecting the `Promise`, always treat it like an uncaught exception. In particular, this also ensures a non-zero exit code. Refs: #34558 PR-URL: #34640 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Myles Borins <[email protected]> Reviewed-By: Yongsheng Zhang <[email protected]>
1 parent 62bb2e7 commit 35ef990

File tree

8 files changed

+111
-14
lines changed

8 files changed

+111
-14
lines changed

doc/api/process.md

+2
Original file line numberDiff line numberDiff line change
@@ -2566,6 +2566,8 @@ cases:
25662566
and generally can only happen during development of Node.js itself.
25672567
* `12` **Invalid Debug Argument**: The `--inspect` and/or `--inspect-brk`
25682568
options were set, but the port number chosen was invalid or unavailable.
2569+
* `13` **Unfinished Top-Level Await**: `await` was used outside of a function
2570+
in the top-level code, but the passed `Promise` never resolved.
25692571
* `>128` **Signal Exits**: If Node.js receives a fatal signal such as
25702572
`SIGKILL` or `SIGHUP`, then its exit code will be `128` plus the
25712573
value of the signal code. This is a standard POSIX practice, since

lib/internal/modules/run_main.js

+16-3
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,23 @@ function shouldUseESMLoader(mainPath) {
4040
function runMainESM(mainPath) {
4141
const esmLoader = require('internal/process/esm_loader');
4242
const { pathToFileURL } = require('internal/url');
43-
esmLoader.loadESM((ESMLoader) => {
43+
handleMainPromise(esmLoader.loadESM((ESMLoader) => {
4444
const main = path.isAbsolute(mainPath) ?
4545
pathToFileURL(mainPath).href : mainPath;
4646
return ESMLoader.import(main);
47-
});
47+
}));
48+
}
49+
50+
function handleMainPromise(promise) {
51+
// Handle a Promise from running code that potentially does Top-Level Await.
52+
// In that case, it makes sense to set the exit code to a specific non-zero
53+
// value if the main code never finishes running.
54+
function handler() {
55+
if (process.exitCode === undefined)
56+
process.exitCode = 13;
57+
}
58+
process.on('exit', handler);
59+
return promise.finally(() => process.off('exit', handler));
4860
}
4961

5062
// For backwards compatibility, we have to run a bunch of
@@ -62,5 +74,6 @@ function executeUserEntryPoint(main = process.argv[1]) {
6274
}
6375

6476
module.exports = {
65-
executeUserEntryPoint
77+
executeUserEntryPoint,
78+
handleMainPromise,
6679
};

lib/internal/process/execution.js

+5-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
const {
44
JSONStringify,
5-
PromiseResolve,
65
} = primordials;
76

87
const path = require('path');
@@ -43,20 +42,15 @@ function evalModule(source, print) {
4342
if (print) {
4443
throw new ERR_EVAL_ESM_CANNOT_PRINT();
4544
}
46-
const { log, error } = require('internal/console/global');
47-
const { decorateErrorStack } = require('internal/util');
48-
const asyncESM = require('internal/process/esm_loader');
49-
PromiseResolve(asyncESM.ESMLoader).then(async (loader) => {
45+
const { log } = require('internal/console/global');
46+
const { loadESM } = require('internal/process/esm_loader');
47+
const { handleMainPromise } = require('internal/modules/run_main');
48+
handleMainPromise(loadESM(async (loader) => {
5049
const { result } = await loader.eval(source);
5150
if (print) {
5251
log(result);
5352
}
54-
})
55-
.catch((e) => {
56-
decorateErrorStack(e);
57-
error(e);
58-
process.exit(1);
59-
});
53+
}));
6054
}
6155

6256
function evalScript(name, body, breakFirstLine, print) {
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import '../common/index.mjs';
2+
import assert from 'assert';
3+
import child_process from 'child_process';
4+
import fixtures from '../common/fixtures.js';
5+
6+
{
7+
// Unresolved TLA promise, --eval
8+
const { status, stdout, stderr } = child_process.spawnSync(
9+
process.execPath,
10+
['--input-type=module', '--eval', 'await new Promise(() => {})'],
11+
{ encoding: 'utf8' });
12+
assert.deepStrictEqual([status, stdout, stderr], [13, '', '']);
13+
}
14+
15+
{
16+
// Rejected TLA promise, --eval
17+
const { status, stdout, stderr } = child_process.spawnSync(
18+
process.execPath,
19+
['--input-type=module', '-e', 'await Promise.reject(new Error("Xyz"))'],
20+
{ encoding: 'utf8' });
21+
assert.deepStrictEqual([status, stdout], [1, '']);
22+
assert.match(stderr, /Error: Xyz/);
23+
}
24+
25+
{
26+
// Unresolved TLA promise with explicit exit code, --eval
27+
const { status, stdout, stderr } = child_process.spawnSync(
28+
process.execPath,
29+
['--input-type=module', '--eval',
30+
'process.exitCode = 42;await new Promise(() => {})'],
31+
{ encoding: 'utf8' });
32+
assert.deepStrictEqual([status, stdout, stderr], [42, '', '']);
33+
}
34+
35+
{
36+
// Rejected TLA promise with explicit exit code, --eval
37+
const { status, stdout, stderr } = child_process.spawnSync(
38+
process.execPath,
39+
['--input-type=module', '-e',
40+
'process.exitCode = 42;await Promise.reject(new Error("Xyz"))'],
41+
{ encoding: 'utf8' });
42+
assert.deepStrictEqual([status, stdout], [1, '']);
43+
assert.match(stderr, /Error: Xyz/);
44+
}
45+
46+
{
47+
// Unresolved TLA promise, module file
48+
const { status, stdout, stderr } = child_process.spawnSync(
49+
process.execPath,
50+
[fixtures.path('es-modules/tla/unresolved.mjs')],
51+
{ encoding: 'utf8' });
52+
assert.deepStrictEqual([status, stdout, stderr], [13, '', '']);
53+
}
54+
55+
{
56+
// Rejected TLA promise, module file
57+
const { status, stdout, stderr } = child_process.spawnSync(
58+
process.execPath,
59+
[fixtures.path('es-modules/tla/rejected.mjs')],
60+
{ encoding: 'utf8' });
61+
assert.deepStrictEqual([status, stdout], [1, '']);
62+
assert.match(stderr, /Error: Xyz/);
63+
}
64+
65+
{
66+
// Unresolved TLA promise, module file
67+
const { status, stdout, stderr } = child_process.spawnSync(
68+
process.execPath,
69+
[fixtures.path('es-modules/tla/unresolved-withexitcode.mjs')],
70+
{ encoding: 'utf8' });
71+
assert.deepStrictEqual([status, stdout, stderr], [42, '', '']);
72+
}
73+
74+
{
75+
// Rejected TLA promise, module file
76+
const { status, stdout, stderr } = child_process.spawnSync(
77+
process.execPath,
78+
[fixtures.path('es-modules/tla/rejected-withexitcode.mjs')],
79+
{ encoding: 'utf8' });
80+
assert.deepStrictEqual([status, stdout], [1, '']);
81+
assert.match(stderr, /Error: Xyz/);
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
process.exitCode = 42;
2+
await Promise.reject(new Error('Xyz'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
await Promise.reject(new Error('Xyz'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
process.exitCode = 42;
2+
await new Promise(() => {});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
await new Promise(() => {});

0 commit comments

Comments
 (0)