From 4fe699cc50eca45125f4f53caa6c0addd9153c0c Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 1 Oct 2026 18:00:20 -0700 Subject: [PATCH] Fix exception propagation when preparing await Resume the suspended async function with throw_flag when PromiseResolve, callback creation, or promise reaction allocation fails. Rejecting its outer promise directly skips catch and finally blocks around await. Add a regression test for exception identity, synchronous catch/finally execution, repeated failures, and recovery after a successful await. Fixes #1757 --- quickjs.c | 13 +++++---- tests/bug1757.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 tests/bug1757.js diff --git a/quickjs.c b/quickjs.c index ed65ce420..1d16cf883 100644 --- a/quickjs.c +++ b/quickjs.c @@ -21505,9 +21505,9 @@ static bool js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s) bool is_success = true; JSValue func_ret, ret2; + resume: func_ret = async_func_resume(ctx, &s->func_state); if (JS_IsException(func_ret)) { - fail: if (unlikely(JS_IsUncatchableError(ctx->rt->current_exception))) { is_success = false; } else { @@ -21546,10 +21546,10 @@ static bool js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s) 1, vc(&value), 0); JS_FreeValue(ctx, value); if (JS_IsException(promise)) - goto fail; + goto await_fail; if (js_async_function_resolve_create(ctx, s, resolving_funcs)) { JS_FreeValue(ctx, promise); - goto fail; + goto await_fail; } /* Note: no need to create 'thrownawayCapability' as in @@ -21562,8 +21562,11 @@ static bool js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s) JS_FreeValue(ctx, promise); for(i = 0; i < 2; i++) JS_FreeValue(ctx, resolving_funcs[i]); - if (res) - goto fail; + if (res) { + await_fail: + s->func_state.throw_flag = true; + goto resume; + } } } return is_success; diff --git a/tests/bug1757.js b/tests/bug1757.js new file mode 100644 index 000000000..a18c165af --- /dev/null +++ b/tests/bug1757.js @@ -0,0 +1,69 @@ +// https://github.com/quickjs-ng/quickjs/issues/1757 +import { assert } from "./assert.js"; + +const error = new Error("boom"); +const promise = Promise.resolve(1); +let calls = 0; +Object.defineProperty(promise, "constructor", { + get() { + calls++; + throw error; + }, +}); + +const log = []; +const result = (async () => { + for (let i = 0; i < 2; i++) { + try { + await promise; + assert(false); + } catch (e) { + assert(e === error); + log.push("catch"); + } finally { + log.push("finally"); + } + } + assert(await Promise.resolve(42), 42); + try { + await promise; + assert(false); + } catch (e) { + assert(e === error); + log.push("resumed catch"); + } + return 123; +})(); + +// PromiseResolve throws before Await suspends the execution context. +assert(log.join(","), "catch,finally,catch,finally"); +assert(await result, 123); +assert(log.join(","), "catch,finally,catch,finally,resumed catch"); +assert(calls, 3); + +let finalized = false; +const rejected = (async () => { + try { + await promise; + } finally { + await 0; + finalized = true; + } +})(); +let caught = false; +try { + await rejected; +} catch (e) { + assert(e === error); + caught = true; +} +assert(caught); +assert(finalized); + +assert(await (async () => { + try { + return await promise; + } finally { + return 456; + } +})(), 456);