From 5f70edcec5a49f0625c6918ef8a366d7d6378592 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:00:26 +0000 Subject: [PATCH] JSC: propagate async context through PromiseFinallyAwaitJob When a .finally() callback returns a thenable, the follow-up that propagates the original fulfilment (or rejects with the thenable's reason) runs as InternalMicrotask::PromiseFinallyAwaitJob. Unlike PromiseFinallyReactionJob a few lines above it, this case did not install the async context, and the phase-1 handler did not capture it on the reaction it schedules, so an unhandled rejection originating here observed an undefined AsyncLocalStorage store. Capture the active async context alongside the reaction at the schedule points in promiseFinallyReactionJob, and install/restore it in the PromiseFinallyAwaitJob case, mirroring PromiseFinallyReactionJob. The synchronous promiseFinallyAwaitJob calls remain inside the phase-1 install window and are unaffected. --- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 51d300d2ded5d..9d1fca9d3d666 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -800,11 +800,22 @@ static void promiseFinallyReactionJob(JSGlobalObject* globalObject, VM& vm, JSPr context->setHandlerOrContext(vm, valueOrReason); context->setPerCellBit(status == JSPromise::Status::Fulfilled); +#if USE(BUN_JSC_ADDITIONS) + // PromiseFinallyAwaitJob may run as a later microtask (scheduled below via + // performPromiseThenWithInternalMicrotask or + // createResolvingFunctionsWithInternalMicrotask), after this call's async + // context has been unwound. Capture it with the reaction so that phase 2 + // can restore it, like PromiseFinallyReactionJob does for phase 1. + JSValue scheduledContext = AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, context); +#else + JSValue scheduledContext = context; +#endif + if (result.inherits()) { auto* promise = uncheckedDowncast(result); if (promise->realm() == globalObject && promise->isThenFastAndNonObservable()) { scope.release(); - promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseFinallyAwaitJob, resultPromise, context); + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseFinallyAwaitJob, resultPromise, scheduledContext); return; } } @@ -846,7 +857,7 @@ static void promiseFinallyReactionJob(JSGlobalObject* globalObject, VM& vm, JSPr return; } - auto [resolve, reject] = JSPromise::createResolvingFunctionsWithInternalMicrotask(vm, globalObject, InternalMicrotask::PromiseFinallyAwaitJob, context); + auto [resolve, reject] = JSPromise::createResolvingFunctionsWithInternalMicrotask(vm, globalObject, InternalMicrotask::PromiseFinallyAwaitJob, scheduledContext); scope.release(); promiseResolveThenableJob(globalObject, resolutionObject, then, resolve, reject); } @@ -1967,8 +1978,13 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas // arguments[0] = unused (we get resultPromise from context) // arguments[1] = settled value from onFinally's result // arguments[2] = context (JSSlimPromiseReaction: promise=resultPromise, handlerOrContext=originalValue, perCellBit=wasFulfilled) + // OR InternalFieldTuple: [context, asyncContext] when Bun async context is present // payload = status of onFinally's result - auto* context = uncheckedDowncast(arguments[2]); + JSValue contextArg = arguments[2]; +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif + auto* context = uncheckedDowncast(contextArg); auto* resultPromise = uncheckedDowncast(context->promise()); scope.release(); promiseFinallyAwaitJob(resultPromise->realm(), vm,