From 279d42e5624436b28d70178e95e4098abbab6c6a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 27 Oct 2026 21:11:41 -0700 Subject: [PATCH] Lower a discarded `undefined` catch handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Promise.catch(() => undefined)` tripped the IR validator's bare-unitLit rule: the handler body lowered as a bare expression, and `undefined` has no standalone IR value. The handler's value is discarded, so lower the body as a statement — the same rule a discarded `undefined;` statement already follows. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-calls.ts | 5 ++- tests/corpus/promise-void-catch-undefined.ts | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/promise-void-catch-undefined.ts diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 6229917a2..45d470c60 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -6908,7 +6908,10 @@ export function lowerPromiseMethodCall( if (ts.isBlock(hb)) { catchBody = lowerer.lowerStmts(hb.statements.nodes); } else if (R.kind === "void") { - catchBody = [{ kind: "exprStmt", expr: lowerer.lowerExpr(hb), loc: locOf(hb) }]; + // The handler's value is discarded, so the body lowers as a + // statement: `() => undefined` is a pure no-op (a bare unit + // literal has no IR value), not an expression to evaluate. + catchBody = [lowerer.lowerExprStatement(hb)]; } else { // Bare-expression handler: `(e) => v` (promise results flatten // through the async-return path, like any `return v`). diff --git a/tests/corpus/promise-void-catch-undefined.ts b/tests/corpus/promise-void-catch-undefined.ts new file mode 100644 index 000000000..ee7285d8a --- /dev/null +++ b/tests/corpus/promise-void-catch-undefined.ts @@ -0,0 +1,31 @@ +// `Promise.catch(() => undefined)` — the handler's value is +// discarded and the recovered promise settles as void. The handler runs +// exactly once, and the chain continues on the next turn whether the +// receiver fulfilled or rejected. + +const ok: Promise = Promise.resolve(); +const bad: Promise = Promise.reject(new Error("boom")); + +ok.catch(() => undefined).then(() => console.log("fulfilled recovered")); + +bad + .catch(() => { + console.log("rejected recovered"); + }) + .then(() => console.log("rejected continued")); + +let runs = 0; + +function recover(): Promise { + runs++; + return runs % 2 === 1 ? Promise.reject(new Error("odd")) : Promise.resolve(); +} + +async function run(): Promise { + await recover().catch(() => undefined); + console.log("awaited recover", runs); + await recover().catch(() => undefined); + console.log("awaited recover", runs); +} + +void run().then(() => console.log("done"));