Skip to content

Consolidate AsyncLocalStorage save/restore with an RAII AsyncContextSwapScope - #301

Merged
Jarred-Sumner merged 4 commits into
mainfrom
farm/9d7837a3/async-context-swap-scope
Jul 17, 2026
Merged

Consolidate AsyncLocalStorage save/restore with an RAII AsyncContextSwapScope#301
Jarred-Sumner merged 4 commits into
mainfrom
farm/9d7837a3/async-context-swap-scope

Conversation

@robobun

@robobun robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Bun's AsyncLocalStorage threads its context through the promise/microtask internals via JSGlobalObject::m_asyncContextData (an InternalFieldTuple, field 0 holds the current context). The restore side of this, in runtime/JSMicrotask.cpp, had the same 8-line "read tuple, save current, swap in, run, restore" block copy-pasted across ~13 InternalMicrotask cases (147 mentions of asyncContext in the file), with the same pattern repeated on the capture side in JSPromise.cpp / JSPromisePrototype.cpp. Every upstream change that reshapes a microtask case is a merge conflict and a chance to silently drop a restore.

This introduces a small header-only RAII helper, AsyncContextSwapScope, and mechanically replaces every open-coded block with it.

The helper

runtime/AsyncContextSwapScope.h (guarded by USE(BUN_JSC_ADDITIONS)):

  • ctor (VM&, JSGlobalObject*, JSValue asyncContext): swaps asyncContext into m_asyncContextData field 0 and remembers the previous value; a no-op when asyncContext is empty or undefined (single branch on the common path).
  • dtor / restoreEarly(): restores the previous value. restoreEarly() is for the two call sites whose existing ordering restores before the tail of the case (AsyncFunctionResume's promise->reject/resolve, BunPerformMicrotaskJob's error reporting).
  • unwrapContextTuple(JSValue&): if the argument is an InternalFieldTuple [userContext, asyncContext], replaces it with field 0 and returns field 1; otherwise leaves it unchanged and returns jsUndefined(). Tolerates an empty JSValue (the dynamicDowncast<T>(JSValue) overload is not empty-safe by itself, and PromiseReactionJob's arguments[3] can be empty).
  • current(JSGlobalObject*) / wrapWithCurrent(VM&, JSGlobalObject*, JSValue userContext): capture-side helpers for reading the current context and wrapping it alongside a user context.

All methods are ALWAYS_INLINE; the class is WTF_FORBID_HEAP_ALLOCATION / WTF_MAKE_NONCOPYABLE.

Behavior

Strictly behavior-preserving, with two deliberate exceptions noted in the commits:

  • PromiseReactionJob had two RETURN_IF_EXCEPTION paths (after promiseOrCapability.get(reject) / .get(resolve) throws) that returned without restoring; the RAII scope now restores there as well.
  • performPromiseThenWithContext is not switched to wrapWithCurrent: it must keep wrapping whenever userContext is defined even if no async context is active, because callers may pass their own InternalFieldTuple as userContext (ReadableStream's async iterator does) and PromiseReactionJob would otherwise unwrap it as [_, asyncContext]. It now reads the current context via AsyncContextSwapScope::current() but keeps its original wrapping condition.

The zero-cost-when-unused properties are preserved: slim / inline reactions still apply when the context is empty, no new fields on reactions, no new allocations on any path that did not already allocate, no changes to existing [[likely]] / early-out fast paths.

Diff shape

 runtime/AsyncContextSwapScope.h   | 122 ++++++++++ (new)
 runtime/JSMicrotask.cpp           |  42 +++- / 326 ----
 runtime/JSPromise.cpp             |  14 +- / 45 ----
 runtime/JSPromisePrototype.cpp    |   2 +- / 10 ----
 CMakeLists.txt                    |   1 +

Net: ~328 lines removed across the three .cpp files; asyncContext* mentions in JSMicrotask.cpp drop from 147 to 21.

Verification

Built Bun against this branch via bun run build:local (debug + ASAN, ASSERT_ENABLED). All of the following pass identically to the prebuilt-WebKit baseline:

  • test/js/node/async_hooks/ (AsyncLocalStorage.test.ts, AsyncLocalStorage-tracking.test.ts, async-local-storage-thenable.test.ts, async-context/): 111 pass / 0 fail / 3 todo
  • Vendored Node parallel tests test-async-local-storage-{bind,contexts,deep-stack,enter-with,exit-does-not-leak,http-multiclients,snapshot}.js, test-http2-async-local-storage.js, test-stream-finished-async-local-storage.js: all exit 0
  • test/js/web/streams/streams.test.js: 157 pass / 2 fail (same two pre-existing timeouts as baseline)

Commits

Split for review:

  1. Introduce AsyncContextSwapScope (no callers)
  2. Capture side: JSPromise.cpp / JSPromisePrototype.cpp
  3. Restore side: JSMicrotask.cpp

robobun added 3 commits July 16, 2026 20:00
Introduces a small stack-only RAII helper that encapsulates the
"read tuple, save current, swap in, run, restore" pattern currently
open-coded at every InternalMicrotask case that runs user code under
a captured AsyncLocalStorage context.

The helper is header-only and ALWAYS_INLINE so codegen is unchanged;
the fast path (asyncContext undefined) remains a single branch. Static
helpers cover the adjacent snapshot-side patterns (reading the current
context and wrapping/unwrapping it in an InternalFieldTuple alongside
a user context).

No callers yet; the mechanical replacements follow in separate commits.
…PromisePrototype)

Mechanical replacement of the snapshot-side blocks with the static helpers
introduced in the previous commit:

- performPromiseThen / resolveWithInternalMicrotaskForAsyncAwait /
  JSPromisePrototype finally(): wrapWithCurrent(vm, globalObject, userContext)
- resolvePromise thenable-job queuing: current(globalObject)

performPromiseThenWithContext keeps its original "wrap whenever userContext
is defined" rule (using current() for the asyncContext read) rather than
wrapWithCurrent: callers may pass their own InternalFieldTuple as userContext
(ReadableStream async iterator does), and PromiseReactionJob would otherwise
mistake it for a [_, asyncContext] wrapper.

No semantic change.
….cpp

Mechanical replacement of the per-InternalMicrotask-case open-coded
"read tuple, save current, swap in, run, restore" blocks with
AsyncContextSwapScope (plus unwrapContextTuple where the async context
arrives as field 1 of an InternalFieldTuple).

AsyncFunctionResume's error / completed-generator branches keep their
existing "restore before promise->reject / promise->resolve" ordering via
restoreEarly(), as does BunPerformMicrotaskJob's "restore before error
reporting" step. Everywhere else the scope naturally restores at return.

PromiseReactionJob previously had no restore on the two
RETURN_IF_EXCEPTION paths after promiseOrCapability.get(reject / resolve)
throws; the RAII scope now restores on those paths as well. All other
call sites are byte-for-byte equivalent when the scope is a no-op
(asyncContext empty or undefined), which is the common path.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa081435-cd8e-4692-863b-2fcf06cbb7e8

📥 Commits

Reviewing files that changed from the base of the PR and between 401f368 and eb0feb8.

📒 Files selected for processing (1)
  • Source/JavaScriptCore/runtime/JSMicrotask.cpp

Walkthrough

Adds AsyncContextSwapScope for RAII-based async-context management, stages the new header, and applies it to promise context capture and internal microtask execution paths under USE(BUN_JSC_ADDITIONS).

Changes

Async context propagation

Layer / File(s) Summary
Async context scope helper
Source/JavaScriptCore/runtime/AsyncContextSwapScope.h, Source/JavaScriptCore/CMakeLists.txt
Adds and stages AsyncContextSwapScope with tuple helpers, current-context lookup, scoped restoration, and early restoration.
Promise context capture and scheduling
Source/JavaScriptCore/runtime/JSPromise.cpp, Source/JavaScriptCore/runtime/JSPromisePrototype.cpp
Uses the helper for promise reactions, thenables, async-await scheduling, and Promise.prototype.finally.
Microtask context execution
Source/JavaScriptCore/runtime/JSMicrotask.cpp
Replaces manual async-context extraction and restoration across promise, async function, async generator, Bun, and module microtask paths with scoped swaps.

Possibly related PRs

  • oven-sh/WebKit#268: Both modify async-context restoration timing in the AsyncFunctionResume microtask path.
  • oven-sh/WebKit#278: Both change promise reaction handling when async context data is active.

Suggested reviewers: constellation

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the required Bugzilla/commit-message template and omits the bug and Reviewed by fields. Rewrite it in the required template with a bug title, Bugzilla link, Reviewed by line, fix explanation, and changed-file bullets.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: consolidating AsyncLocalStorage save/restore into an RAII scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Source/JavaScriptCore/runtime/JSMicrotask.cpp`:
- Around line 1616-1620: Move the AsyncContextSwapScope initialization in the
promise resolution job so it is established before the
promiseSpeciesWatchpointIsValid check and any call to
promiseResolveThenableJobFastSlow. Preserve the existing arguments[2] context
source and ensure both fast and slow paths execute within the captured context.
- Around line 1887-1890: Update the Async-from-Sync continuation in the code
around AsyncContextSwapScope::unwrapContextTuple to retain and apply the tuple’s
field 1 as the async context before invoking
asyncFromSyncIteratorContinueOrDone. Ensure the captured context from await is
installed for the continuation instead of being discarded, while preserving the
existing promise and completion handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ecca483d-9740-47d8-acc6-375c0fefc1cc

📥 Commits

Reviewing files that changed from the base of the PR and between e5f7fc2 and 401f368.

📒 Files selected for processing (5)
  • Source/JavaScriptCore/CMakeLists.txt
  • Source/JavaScriptCore/runtime/AsyncContextSwapScope.h
  • Source/JavaScriptCore/runtime/JSMicrotask.cpp
  • Source/JavaScriptCore/runtime/JSPromise.cpp
  • Source/JavaScriptCore/runtime/JSPromisePrototype.cpp

Comment on lines 1616 to +1620
if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]]
RELEASE_AND_RETURN(scope, promiseResolveThenableJobFastSlow(globalObject, promise, promiseToResolve));

#if USE(BUN_JSC_ADDITIONS)
// Set up async context for promise resolution
InternalFieldTuple* asyncContextData = nullptr;
JSValue restoreAsyncContext;
if (!asyncContext.isUndefined()) {
asyncContextData = globalObject->m_asyncContextData.get();
if (asyncContextData) {
restoreAsyncContext = asyncContextData->getInternalField(0);
asyncContextData->putInternalField(vm, 0, asyncContext);
}
}
AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[2]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Establish the context scope before entering the slow fallback.

When the species watchpoint is invalid, Line 1617 calls promiseResolveThenableJobFastSlow before arguments[2] is installed. Observable species/then operations therefore run outside the context captured when this job was queued.

Proposed fix
+#if USE(BUN_JSC_ADDITIONS)
+        AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[2]);
+#endif
+
         if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]]
             RELEASE_AND_RETURN(scope, promiseResolveThenableJobFastSlow(globalObject, promise, promiseToResolve));
-
-#if USE(BUN_JSC_ADDITIONS)
-        AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[2]);
-#endif
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]]
RELEASE_AND_RETURN(scope, promiseResolveThenableJobFastSlow(globalObject, promise, promiseToResolve));
#if USE(BUN_JSC_ADDITIONS)
// Set up async context for promise resolution
InternalFieldTuple* asyncContextData = nullptr;
JSValue restoreAsyncContext;
if (!asyncContext.isUndefined()) {
asyncContextData = globalObject->m_asyncContextData.get();
if (asyncContextData) {
restoreAsyncContext = asyncContextData->getInternalField(0);
asyncContextData->putInternalField(vm, 0, asyncContext);
}
}
AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[2]);
`#if` USE(BUN_JSC_ADDITIONS)
AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[2]);
`#endif`
if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]]
RELEASE_AND_RETURN(scope, promiseResolveThenableJobFastSlow(globalObject, promise, promiseToResolve));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/runtime/JSMicrotask.cpp` around lines 1616 - 1620, Move
the AsyncContextSwapScope initialization in the promise resolution job so it is
established before the promiseSpeciesWatchpointIsValid check and any call to
promiseResolveThenableJobFastSlow. Preserve the existing arguments[2] context
source and ensure both fast and slow paths execute within the captured context.

Comment on lines 1887 to 1890
JSValue contextArg = arguments[2];
if (auto* tuple = dynamicDowncast<InternalFieldTuple>(contextArg))
contextArg = tuple->getInternalField(0);
AsyncContextSwapScope::unwrapContextTuple(contextArg);
auto* promise = uncheckedDowncast<JSPromise>(asObject(contextArg)->getDirect(vm, vm.propertyNames->builtinNames().promisePrivateName()));
RELEASE_AND_RETURN(scope, asyncFromSyncIteratorContinueOrDone(promise->realm(), vm, promise, contextArg, arguments[1], static_cast<JSPromise::Status>(payload), task == InternalMicrotask::AsyncFromSyncIteratorDone));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply the unwrapped async context instead of discarding it.

Line 1888 strips the tuple but ignores field 1, so these Async-from-Sync continuations execute under the ambient context rather than the context captured at await.

Proposed fix
 `#if` USE(BUN_JSC_ADDITIONS)
         JSValue contextArg = arguments[2];
-        AsyncContextSwapScope::unwrapContextTuple(contextArg);
+        AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg));
         auto* promise = uncheckedDowncast<JSPromise>(asObject(contextArg)->getDirect(vm, vm.propertyNames->builtinNames().promisePrivateName()));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JSValue contextArg = arguments[2];
if (auto* tuple = dynamicDowncast<InternalFieldTuple>(contextArg))
contextArg = tuple->getInternalField(0);
AsyncContextSwapScope::unwrapContextTuple(contextArg);
auto* promise = uncheckedDowncast<JSPromise>(asObject(contextArg)->getDirect(vm, vm.propertyNames->builtinNames().promisePrivateName()));
RELEASE_AND_RETURN(scope, asyncFromSyncIteratorContinueOrDone(promise->realm(), vm, promise, contextArg, arguments[1], static_cast<JSPromise::Status>(payload), task == InternalMicrotask::AsyncFromSyncIteratorDone));
JSValue contextArg = arguments[2];
AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg));
auto* promise = uncheckedDowncast<JSPromise>(asObject(contextArg)->getDirect(vm, vm.propertyNames->builtinNames().promisePrivateName()));
RELEASE_AND_RETURN(scope, asyncFromSyncIteratorContinueOrDone(promise->realm(), vm, promise, contextArg, arguments[1], static_cast<JSPromise::Status>(payload), task == InternalMicrotask::AsyncFromSyncIteratorDone));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/runtime/JSMicrotask.cpp` around lines 1887 - 1890,
Update the Async-from-Sync continuation in the code around
AsyncContextSwapScope::unwrapContextTuple to retain and apply the tuple’s field
1 as the async context before invoking asyncFromSyncIteratorContinueOrDone.
Ensure the captured context from await is installed for the continuation instead
of being discarded, while preserving the existing promise and completion
handling.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any bugs, but this is a ~450-line refactor across the promise/microtask hot path with a few intentional behavior changes, so it's worth a human look.

What was reviewed:

  • Traced each of the ~13 runInternalMicrotask cases: RAII destructor ordering matches the old explicit-restore ordering (including restoreEarly() sites and the unified non-Bun #else branches).
  • unwrapContextTuple vs. old open-coded extraction in PromiseReactionJob: empty/null/non-cell arguments[3] still routes to the 1-arg callMicrotask branch.
  • performPromiseThenWithContext: dropping the outer m_asyncContextData null-guard now wraps a defined userContext even when tracking is disabled — this is a fix (protects a user-supplied InternalFieldTuple from being mis-unwrapped downstream), consistent with the PR's stated rationale, but not called out separately.
  • Confirmed the 4-arg InternalFieldTuple::create overload exists.
Extended reasoning...

Overview

Introduces a header-only RAII helper AsyncContextSwapScope (new file, 122 lines) and mechanically replaces ~13 open-coded save/swap/restore blocks in JSMicrotask.cpp plus 4 capture-side blocks in JSPromise.cpp / JSPromisePrototype.cpp. Net -328 lines. All code is guarded by USE(BUN_JSC_ADDITIONS), but several #if/#else branches are unified so the non-Bun path is also reshaped (verified equivalent — RELEASE_AND_RETURN on a void call ≡ scope.release(); call(); return;).

Security risks

None identified. This is internal AsyncLocalStorage context plumbing; no user-controlled input parsing, no auth/crypto, no boundary crossing. The RAII helper is WTF_FORBID_HEAP_ALLOCATION / WTF_MAKE_NONCOPYABLE and holds a raw InternalFieldTuple* only for the stack lifetime of a single microtask dispatch.

Level of scrutiny

High. runInternalMicrotask / performPromiseThen / resolvePromise are on the hot path for every await, .then, and microtask in Bun. A dropped restore or a mis-ordered destructor here corrupts AsyncLocalStorage for the rest of the event loop tick, and a stray allocation regresses promise throughput. The PR is a mechanical refactor and the author ran the full async_hooks / Node parallel suite under debug+ASAN, but the three intentional behavior changes (two RETURN_IF_EXCEPTION paths in PromiseReactionJob now restore; performPromiseThenWithContext now wraps whenever userContext is defined even if m_asyncContextData is null) deserve human sign-off.

Other factors

  • I walked each converted case and confirmed the RAII destructor fires at the same point the old explicit putInternalField(vm, 0, restoreAsyncContext) did, including the two restoreEarly() call sites (AsyncFunctionResume reject/resolve, BunPerformMicrotaskJob error reporting).
  • PromiseReactionJob: old code normalized empty/undefined/null contextArg to userContext = jsUndefined(); new code leaves it as-is but the downstream userContext.isEmpty() || userContext.isUndefinedOrNull() branch treats all three identically, so no observable change.
  • unwrapContextTuple guards contextArg.isEmpty() before dynamicDowncast, matching the old explicit check.
  • performPromiseThenWithContext: the dropped outer if (m_asyncContextData) guard means a defined userContext is now always tuple-wrapped even when async-context tracking was never enabled. This is the fix the PR description alludes to (prevents a caller-supplied InternalFieldTuple — e.g. ReadableStream async iterator — from being mis-unwrapped in PromiseReactionJob), but it's a behavior change on a path the listed test suites may not exercise with tracking disabled.
  • Verified InternalFieldTuple::create(vm, structure, a, b) overload exists (used by wrapWithCurrent and the rewritten performPromiseThenWithContext).

Given the criticality of the touched code and the presence of deliberate behavior changes, deferring rather than auto-approving.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun fix the errors

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any bugs, but this refactor touches the core promise/microtask dispatch path across ~13 InternalMicrotask cases, so it warrants a human look before merging.

What was reviewed:

  • Verified each replaced block in JSMicrotask.cpp preserves the prior swap/restore ordering (including restoreEarly() at the two sites that restored before the tail).
  • Confirmed the two CodeRabbit flags (PromiseResolveThenableJobFast slow-path ordering, AsyncFromSyncIterator discarding field 1) match pre-PR behavior — not regressions introduced here.
  • Checked unwrapContextTuple's empty-JSValue guard against PromiseReactionJob's possibly-empty arguments[3].
  • Noted performPromiseThenWithContext now wraps when userContext is defined even if m_asyncContextData is null (previously it did not) — consistent with the stated intent, but a real behavior delta.
Extended reasoning...

Overview

Introduces a header-only RAII helper AsyncContextSwapScope and replaces ~13 open-coded "read tuple / save / swap / run / restore" blocks across JSMicrotask.cpp, JSPromise.cpp, and JSPromisePrototype.cpp. Net ~328 lines removed. All changes are guarded by USE(BUN_JSC_ADDITIONS).

Security risks

None identified. This is internal engine plumbing for AsyncLocalStorage context propagation; no parsing, auth, or externally-controlled data flow is involved.

Level of scrutiny

High. This is the hot path for every promise reaction, async function resume, and async generator step in Bun's runtime. A dropped or mis-ordered restore leaks async context across unrelated microtasks, and such bugs are notoriously hard to reproduce. While the transformation is mechanical in intent, each of the 13 sites has slightly different control flow (early returns, RETURN_IF_EXCEPTION, restoreEarly() ordering), and the PR itself documents two intentional behavior changes plus a subtle third in performPromiseThenWithContext (wrapping now happens when m_asyncContextData is unset but userContext is defined).

Other factors

  • The two CodeRabbit "Major" findings both describe pre-existing behavior that the PR faithfully preserves (the old code also swapped after the species-watchpoint check, and also discarded field 1 in AsyncFromSyncIterator); they are not regressions, but the maintainer's follow-up "fix the errors" comment has no visible resolution yet.
  • The four AsyncGenerator* cases dropped their separate non-Bun #else branch in favor of a unified body; I verified the resulting non-Bun path is equivalent (the RELEASE_AND_RETURN became scope.release(); ...; return;).
  • The author reports Bun's async_hooks and vendored Node async-local-storage test suites pass under debug+ASAN, which is good coverage for the happy paths but may not exercise the exception-during-.get(reject) edge or the null-m_asyncContextData delta.

Given the breadth, the criticality of the code path, and the unresolved maintainer comment, deferring to human review.

@Jarred-Sumner
Jarred-Sumner merged commit 8be9955 into main Jul 17, 2026
47 checks passed
@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
eb0feb84 autobuild-preview-pr-301-eb0feb84 2026-07-17 04:18:30 UTC

robobun added a commit that referenced this pull request Jul 17, 2026
…tion, Dockerfile.windows)

JSMicrotask.cpp: adopt the AsyncContextSwapScope RAII helper (#301)
in place of the manual m_asyncContextData save/restore blocks, keeping
upstream's microtaskCallCache threading and the asyncFunctionGeneratorBodyCall
extraction. The AsyncGeneratorDriverResume enqueue sites and dispatch case are
converted to wrapWithCurrent / unwrapContextTuple to match the siblings.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants