Files
scriptc/tests/corpus/stack-overflow-async.ts
T
Chris Tate 3bcf761f42 Throw a catchable RangeError on deep recursion
Native executables used to terminate with a signal when recursion
exhausted the stack. Functions that can take part in unbounded recursion
now compare the stack pointer against a per-stack guard on entry and
throw Node's `RangeError: Maximum call stack size exceeded` through the
ordinary exception path, so catch handlers, finally blocks, worker error
events and the uncaught-error exit code behave as they do under Node.

- The checked set is every function on a cycle of the call graph, where
  operations that can reenter compiled code indirectly (indirect calls,
  callbacks, dynamic and library operations) reach closure targets and
  class members. Non-recursive functions, including leaf callbacks, stay
  check-free, and the may-throw analysis accounts for the new throw.
- The runtime installs the guard per context (main thread from the stack
  rlimit, workers from their thread attributes) and per async fiber, with
  a reserve for the throw, unwinding and error reporting. Worker threads
  now get an explicit 8 MiB stack.
- Scalar call results pass through the unwind path as its dummy value, so
  calls in tail position keep tail-call and recursion elimination.
2026-10-09 16:55:35 -05:00

50 lines
1.2 KiB
TypeScript

// Recursion inside async functions and generators runs on their own stacks;
// exhausting one throws the catchable RangeError there (or rejects).
function depth(n: number): number {
return n === 0 ? 0 : 1 + depth(n - 1);
}
function endless(n: number): number {
return endless(n + 1) + 1;
}
async function guarded(label: string): Promise<string> {
await null;
try {
return String(endless(0));
} catch (e) {
return `${label} ${(e as Error).name} ${(e as Error).message}`;
}
}
async function rejecting(): Promise<number> {
await null;
return endless(0);
}
async function bounded(): Promise<number> {
await null;
return depth(2000);
}
function* steps(): Generator<number> {
yield depth(1000);
try {
yield endless(0);
} catch (e) {
yield (e as Error).name.length;
}
}
console.log(await guarded("fiber"));
try {
await rejecting();
} catch (e) {
console.log("rejected", e instanceof RangeError, (e as Error).message);
}
console.log("bounded", await bounded());
const first = guarded("one");
const second = guarded("two");
console.log(await second, "/", await first);
console.log([...steps()].join(","));
console.log("main", depth(3000));