mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Report unhandled rejections in programs without async work
A program with no async functions, generators or timers never ran the
event loop, so a promise rejection nothing handled went unreported:
`Promise.reject(new Error('x'))` alone exited 0, where Node prints the
error and exits 1. The loop now also runs when the program makes a
promise (a Promise static, `new Promise` or Promise.withResolvers), and
its end reports the rejection, runs the 'exit' listeners with code 1 and
exits 1, as for async programs. Programs that make no promise keep their
code unchanged.
This commit is contained in:
@@ -103,6 +103,7 @@ export interface ChunkResult {
|
||||
readonly flags: {
|
||||
readonly usesCoroutines: boolean;
|
||||
readonly usesTimers: boolean;
|
||||
readonly usesPromises: boolean;
|
||||
readonly usesGenSlots: boolean;
|
||||
readonly needsUnionNone: boolean;
|
||||
readonly needsNullableAbsent: boolean;
|
||||
|
||||
@@ -603,14 +603,16 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
const globalReleases = exitReleaseLines("g");
|
||||
const asyncEntry = emitter.fnByName.get(emitter.mod.entry)?.async === true;
|
||||
const entryMayThrow = emitter.mayThrow.has(emitter.mod.entry);
|
||||
// The event loop runs when timers appeared OR any async/generator
|
||||
// function exists (the C main's hasAsync || hasGenerators ||
|
||||
// usesTimers gate). Generator programs run
|
||||
// The event loop runs when timers appeared, a promise is made (the loop's
|
||||
// end reports an unhandled rejection, Node's exit 1), OR any
|
||||
// async/generator function exists (the C main's hasAsync ||
|
||||
// hasGenerators || usesTimers gate). Generator programs run
|
||||
// the loop too: its exit accounting notes still-suspended generator
|
||||
// fibers as abandoned, so the RC audit downgrades exactly like the
|
||||
// async loop-exhaustion story.
|
||||
const runsLoop =
|
||||
emitter.usesTimers ||
|
||||
emitter.usesPromises ||
|
||||
emitter.ffiHasForeignCallback ||
|
||||
emitter.mod.functions.some((f) => f.async === true || f.generator !== undefined);
|
||||
const uncaughtReleases = entryMayThrow && !asyncEntry ? exitReleaseLines("gu") : [];
|
||||
|
||||
@@ -1635,6 +1635,10 @@ export class LlEmitter {
|
||||
/** setTimeout and friends appeared somewhere: main must run the event
|
||||
* loop even in programs with no async functions. */
|
||||
usesTimers = false;
|
||||
/** A promise is made somewhere (Promise statics, `new Promise`,
|
||||
* Promise.withResolvers): main runs the event loop, whose end reports an
|
||||
* unhandled rejection as Node does, even without async functions. */
|
||||
usesPromises = false;
|
||||
/** The program calls mock.timers members (moduleUsesMockTimers): timer
|
||||
* calls emit scr_test.c's mock-aware entry points. */
|
||||
readonly mockTimers: boolean;
|
||||
@@ -5717,6 +5721,7 @@ export class LlEmitter {
|
||||
flags: {
|
||||
usesCoroutines: this.usesCoroutines,
|
||||
usesTimers: this.usesTimers,
|
||||
usesPromises: this.usesPromises,
|
||||
usesGenSlots: this.usesGenSlots,
|
||||
needsUnionNone: this.needsUnionNone,
|
||||
needsNullableAbsent: this.needsNullableAbsent,
|
||||
@@ -5754,6 +5759,7 @@ export class LlEmitter {
|
||||
const flags = chunk.flags;
|
||||
this.usesCoroutines ||= flags.usesCoroutines;
|
||||
this.usesTimers ||= flags.usesTimers;
|
||||
this.usesPromises ||= flags.usesPromises;
|
||||
this.usesGenSlots ||= flags.usesGenSlots;
|
||||
this.needsUnionNone ||= flags.needsUnionNone;
|
||||
this.needsNullableAbsent ||= flags.needsNullableAbsent;
|
||||
|
||||
@@ -100,6 +100,7 @@ export function emitExpr(host: LlvmEmitterContext, e: IrExpr): LlValue {
|
||||
case "caughtToDyn":
|
||||
return host.emitDynamicExpr(e);
|
||||
case "intrinsic":
|
||||
if (e.name.startsWith("promise.")) host.usesPromises = true;
|
||||
return host.emitIntrinsicExpr(e);
|
||||
case "jsonStringify":
|
||||
case "dynCheck":
|
||||
@@ -108,8 +109,10 @@ export function emitExpr(host: LlvmEmitterContext, e: IrExpr): LlValue {
|
||||
case "genResume":
|
||||
case "awaitExpr":
|
||||
case "awaitUnionExpr":
|
||||
return host.emitAsyncExpr(e);
|
||||
case "newPromise":
|
||||
case "promiseWithResolvers":
|
||||
host.usesPromises = true;
|
||||
return host.emitAsyncExpr(e);
|
||||
case "libCall":
|
||||
return host.emitLibCall(e);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/unhandled-rejection-without-async.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// @exit: 1
|
||||
'use strict';
|
||||
// A program with no async functions or timers still ends Node's way when a
|
||||
// promise rejection goes unhandled: after the script, the 'exit' listeners
|
||||
// see code 1 and the process exits 1. Settled promises alone do not fail.
|
||||
process.on('exit', (code) => console.log('exit listener', code));
|
||||
const settled = Promise.resolve(1);
|
||||
new Promise((resolve) => resolve(2));
|
||||
console.log('end of script', typeof settled);
|
||||
new Promise((resolve, reject) => reject(new TypeError('rejected in the executor')));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2511, "san": 3503 }
|
||||
Reference in New Issue
Block a user