Skip to content

Follow promise resolving functions in the async stack of a native rejection - #43127

Open
robobun wants to merge 3 commits into
mainfrom
robobun/f752aedb/async-stack-through-resolving-functions
Open

robobun wants to merge 3 commits into
mainfrom
robobun/f752aedb/async-stack-through-resolving-functions

Conversation

@robobun

@robobun robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Once JSC leaves its promise fast paths, a native error has no async stack for the rest of the process. await fs.promises.readFile(missing) rejects with e.stack === undefined. Node prints every frame.
  • JSC leaves them for good in three cases: then defined on Object.prototype, Promise.prototype.then replaced, Promise.prototype frozen (SES lockdown()).
  • Cause: JSC then calls inner.then(resolve, reject) to resolve a promise with another promise. The walk in src/jsc/bindings/AsyncStackTrace.cpp follows the promise that then() returned. Nothing awaits it.

Fix

  • A reject handler that is an unused JSC resolving function names where the rejection goes: its promise, or its await. The walk continues there. If nothing awaits there, it uses the promise that then() returned.
  • After a freeze, then() keeps that promise in a capability record, which the walk now reads.
  • Correct: the rejection goes there. V8's CaptureAsyncStackTrace follows PromiseCapabilityDefaultResolve the same way.
  • Verified: test/js/bun/util/bun-file.test.ts (3 new tests, all fail on 1.4.3-canary). Also the fs, streams and stack-trace suites. Self-reviewed: 12 concerns raised, 12 addressed.

Background

  • A native rejection (fs, Bun.file, streams) has no JS call stack. Bun__attachAsyncStackFromPromise builds one from the async functions that await the rejected promise. It finds them through the pending reactions.
  • Every fs.promises function is an async function that returns the native promise. On the fast path one internal reaction links the two promises. The fast paths depend on two realm watchpoints, which never reset once fired.
  • A resolving function is the native resolve or reject of one promise. An internal field holds its promise or its await context.
Notes

How it was found. There is no user report. test/js/web/streams/streams.test.js has a test that defines and deletes Object.prototype.then. Run by hand in one process before test/js/node/fs/promises.test.js, it makes errors from fs.promises include async stack frames fail (-t "multi-chunk|errors from fs.promises include async stack frames": 79 pass, 1 fail on 1.4.3-canary, 80 pass here). CI runs one file per process, so CI cannot see it.

The test. The fixture runs 14 ways to consume a native rejection, before and after the trigger, each with and without AsyncLocalStorage.run(). It runs in a child process because the trigger cannot be undone. The native promise is Bun.file(missing).stat() and not text(): on Windows a text() that rejects does not keep the process alive (#39787), so the fixture printed nothing there. Shapes with no frames on 1.4.3-canary:

phase shapes with no at async frame
before any trigger promiseSubclass, forwardingThenable, thenResolveReject, catchReject
after Object.prototype.then or a replaced Promise.prototype.then the 4 above, fsPromises, asyncFunctionReturn, resolveWithPromise, race, failFastWorker
after Object.freeze(Promise.prototype) all 14

On this branch every shape has its frames in every phase, on Linux and on Windows x64. SES lockdown() was checked by hand: e.stack === undefined on canary, full frames here.

Frames that are new. The first row never had frames. await of a Promise subclass or of a thenable that forwards then(onFulfilled, onRejected) to a native promise uses the third kind of resolving function (promiseResolvingFunctionRejectWithInternalMicrotask), which holds the await context. nativePromise.then(resolve, reject) and .catch(reject) with the functions from Promise.withResolvers() use the same pair kind as Promise.race.

No frame is lost. A first revision followed the resolving function only. It lost await nativePromise.catch(reject) when only a callback consumes the deferred promise. The fallback keeps it (thenResolveRejectResult, catchRejectResult). Each walk has its own limit of 32 hops, so a long chain behind the reject function cannot use up the hops of the fallback (catchRejectResultLongChain). A search has at most 8 walks. If that limit ends a search, one more walk runs without reject functions, which is the search as it is on main (catchRejectResultManyTargets). A probe over 33 shapes, 7 realm states, with and without AsyncLocalStorage (462 cells), compared with 1.4.3-canary: 244 cells are equal, the others gain frames, none loses one.

Cycles. worker().catch(abort), where worker() awaits Promise.race([nativePromise, aborted]), is a cycle: the return promise of worker() leads to aborted, and aborted leads back to the race that worker() awaits. The outer loop had no guard for that. With the reject function followed, the stack was at async worker ten times. The search now treats a generator that it already visited as a dead end and tries the fallbacks, so the result is the same as on main: one worker frame, then the caller (failFastWorker).

Still without frames.

  • A then patch that wraps the callbacks (zone.js, async-listener, continuation-local-storage): the reaction holds the wrapper, not the resolving function. Checked: same result on canary and here. return await in the fs.promises wrappers would cover fs.promises for these. That is a separate change.
  • new Promise((resolve, reject) => nativePromise.then(resolve, reject)): the executor functions are JS closures.
  • Promise.all, any and allSettled on the fast path. Off the fast path JSC registers the reject function of the Promise.all promise, so the walk follows it there.
  • Errors thrown from JS. They get their async stack from JSC's Interpreter::getAsyncStackTrace, not from this walk.
  • Native sites that do not call reject_with_async_stack.

No allocation. The walk runs under AssertNoGC. JSObject::getDirect(vm, name) can materialize a property table, so the capability lookup uses Structure::getConcurrently. The fixture also passes with BUN_JSC_collectContinuously=1 and with BUN_JSC_slowPathAllocsBetweenGCs=20.

Other open PRs on this file. #43014 (finally()) edits the same loop. This diff stays off the lines it touches. The two merge cleanly in either order, and the merged tree passes the tests of both. #38074 and #35685 also merge cleanly. The new test is in bun-file.test.ts and not in promises.test.js for the same reason.

Review. Two findings of the automated review are fixed in the second commit: the shared hop budget and the cycle above.

Self-review. The review asked for: the fallback above, the third resolving-function kind, a species-watchpoint trigger in the test, a narrower statement of who benefits, and no conflict with #43014. All are in. One is done differently than proposed: the review asked to stack this on #43014. The diff avoids its lines instead, so neither PR has to wait for the other.


[human-review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/util/bun-file.test.ts
ninja: Entering directory `/workspace/bun/build/debug'
[1/173] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[2/171] gen NodeModuleModule.lut.h
Generating /workspace/bun/build/debug/codegen/NodeModuleModule.lut.h from /workspace/bun/src/jsc/modules/NodeModuleModule.cpp
[3/171] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, rust=0); 245 extern-C blocks audited
[4/171] gen BunProcess.lut.h
Generating /workspace/bun/build/debug/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[5/171] gen cpp.rs (cppbind)
[6/171] gen BunObject.lut.h
Generating /workspace/bun/build/debug/codegen/BunObject.lut.h from /workspace/bun/src/jsc/bindings/BunObject.cpp
[7/171] gen JS modules (bundle-modules)
Preprocess modules (11197ms)
Bundle modules (56ms)
Postprocesss modules (31ms)
Bundle Functions (622ms)
Generate Code (37ms)

[11.95s] Bundled "src/js" for development
  2799 kb
  197 internal modules
  13 native
... (truncated)

release without fix: 3 FAILED
bun test v1.4.3-canary.1 (c6b7fcb5b)

test/js/bun/util/bun-file.test.ts:
(pass) delete() and stat() should work with unicode paths [2.72ms]
(pass) writer.end() should not close the fd if it does not own the fd [4.46ms]
(pass) Bun.file() read errors include async stack frames [0.38ms]
(pass) Bun.write() errors include async stack frames [1.81ms]
(pass) Bun.file().arrayBuffer() errors include async stack frames [0.28ms]
143 |   const frames = {
144 |     ...Object.fromEntries(shapes.map(shape => [shape, [shape, "asyncFramesOf"]])),
145 |     // One frame for worker(), although the chain leads back to it.
146 |     failFastWorker: ["worker", "failFastWorker", "asyncFramesOf"],
147 |   };
148 |   expect({ result: stdout && JSON.parse(stdout), stderr }).toEqual({
                                                                 ^
error: expect(received).toEqual(expected)

@@ -1,134 +1,45 @@
  {
    "result": {
      "after": {
-       "asyncFunctionReturn": [
-         "asyncFunctionReturn",
-         "asyncFramesOf",
-       ],
-       "catchReject": [
-         "catchReject",
-         "asyncFramesOf",
-       ],
-       "catchRejectResult": [
-         "catchRejectResu
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/util/bun-file.test.ts
bun test v1.4.3 (c6b7fcb5b)

test/js/bun/util/bun-file.test.ts:
(pass) delete() and stat() should work with unicode paths [121.69ms]
(pass) writer.end() should not close the fd if it does not own the fd [148.74ms]
(pass) Bun.file() read errors include async stack frames [20.14ms]
(pass) Bun.write() errors include async stack frames [30.43ms]
(pass) Bun.file().arrayBuffer() errors include async stack frames [16.05ms]
(pass) native rejections keep their async stack after defining Object.prototype.then [1386.13ms]
(pass) native rejections keep their async stack after replacing Promise.prototype.then [1358.31ms]
(pass) native rejections keep their async stack after freezing Promise.prototype [1384.72ms]
(pass) Bun.file().json() with UTF-8 BOM does not free an interior pointer [557.18ms]

 9 pass
 0 fail
 55 expect() calls
Ran 9 tests across 1 file. [5.42s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1761ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/133] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[2/131] gen NodeModuleModule.lut.h
Generating /workspace/bun/build/release/codegen/NodeModuleModule.lut.h from /workspace/bun/src/jsc/modules/NodeModuleModule.cpp
[3/131] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, rust=0); 242 extern-C blocks audited
[4/131] gen cpp.rs (cppbind)
[5/131] gen BunObject.lut.h
Generating /workspace/bun/build/release/codegen/BunObject.lut.h from /workspace/bun/src/jsc/bindings/BunObject.cpp
[6/131] gen BunProcess.lut.h
Generating /workspace/bun/build/release/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[7/131] gen JS modules (bundle-modules)
Preprocess modules (13324ms)
Bundle modules (57ms)
Postprocesss modules (146ms)
Bundle Functions (595ms)
Generate Code (45ms)

[14.18s] Bundled "src/js" for production
  2603 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[7
... (truncated)
diff hotspot
src/jsc/bindings/AsyncStackTrace.cpp               |  94 ++++++++++-
 test/js/bun/util/bun-file.test.ts                  |  47 ++++++
 .../util/native-rejection-async-stack-fixture.js   | 182 +++++++++++++++++++++
 3 files changed, 319 insertions(+), 4 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                      reads  edits  tests
src/jsc/bindings/AsyncStackTrace.cpp                          7     16     21
test/js/bun/util/bun-file.test.ts                             2      2     19
test/js/bun/util/native-rejection-async-stack-fixture.js      2      2     21

…ection

A native rejection gets its async stack from a walk over the pending
reactions of the rejected promise. The walk knew only the reaction layouts
of JSC's promise fast paths. Off those paths JSC calls then(resolve, reject)
with a resolving-function pair, and the walk followed the promise that
then() returned, which nothing awaits.

JSC leaves the fast paths for the rest of the realm once `then` is defined
on Object.prototype, Promise.prototype.then is replaced, or
Promise.prototype is frozen. Every fs.promises error then had no stack,
because each fs.promises function is an async function that returns the
native promise.

The walk now continues at the promise a reject function rejects, or at the
await it resumes, and falls back to the promise then() returned when
nothing awaits that target. It also reads the promise out of the capability
record that then() stores once the species watchpoint has fired.
@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

How to reproduce. A release build is enough.

import { readFile } from "node:fs/promises";
async function level1() { await readFile("/nonexistent-path/x"); }
async function probe(label) { try { await level1(); } catch (e) { console.log(label, e.stack); } }
await probe("before"); // Error: ENOENT ... at async level1 ... at async probe
Object.defineProperty(Object.prototype, "then", { configurable: true, get() {} });
delete Object.prototype.then;
await probe("after"); // undefined on 1.4.3-canary, the same frames on this branch

The same state in the test suite, two files in one process:

bun test test/js/web/streams/streams.test.js test/js/node/fs/promises.test.js -t "multi-chunk|errors from fs.promises include async stack frames"

1.4.3-canary: 79 pass, 1 fail (errors from fs.promises include async stack frames). This branch: 80 pass.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 54e65124-b126-40f8-a7bf-d4f92572f77f

📥 Commits

Reviewing files that changed from the base of the PR and between 75c5690 and af78411.

📒 Files selected for processing (3)
  • src/jsc/bindings/AsyncStackTrace.cpp
  • test/js/bun/util/bun-file.test.ts
  • test/js/bun/util/native-rejection-async-stack-fixture.js

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


Walkthrough

Native Promise rejection tracing now resolves JSC rejection targets, follows reaction and derived-promise paths, prevents cyclic generator traversal, and adds coverage for multiple promise patterns and runtime contexts.

Changes

Async rejection tracing

Layer / File(s) Summary
Resolve rejection targets
src/jsc/bindings/AsyncStackTrace.cpp
Added JSC headers and helpers to identify rejection targets, extract reject handlers, and recover derived promises.
Walk rejection reactions
src/jsc/bindings/AsyncStackTrace.cpp
Updated reaction traversal to follow rejection targets, separate fulfill handling, track fallback promises, bound fallback walks, and prevent repeated generator visits.
Validate native rejection stacks
test/js/bun/util/native-rejection-async-stack-fixture.js, test/js/bun/util/bun-file.test.ts
Added coverage for native rejection propagation across promise patterns, fast-path mutations, and AsyncLocalStorage.run() contexts.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to af784

The native rejection tracing change has targeted coverage for its shared behavior and no actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the main change: following promise resolving functions during native rejection async-stack tracing.
Description check ✅ Passed The description fully explains the problem, implementation, affected scenarios, limitations, and verification results. It does not use the template headings exactly, but it provides the required conte…

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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

2 verified lower-impact observations (convention, logging or cleanup points) were not posted.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 src/jsc/bindings/AsyncStackTrace.cpp — A program that feeds an async function's own outcome back into a race it awaits gets a stack of ten identical at async frames instead of one, with no deadlock required. The outer loop at AsyncStackTrace.cpp:233-238 has no visited set, and the new reject-function following at line 72-75 lets getAwaitingGenerator(returnPromise) land back on the same generator. Fix: stop the outer loop when getAwaitingGenerator returns a generator already appended (track visited generators or return promises), so a cycle yields each frame once.

    Extended reasoning...

    The dismissal said this needs a deadlock-shaped program. It does not. Off the fast path, Promise.race registers the race promise's first-resolving reject function on each input, and rejectionTargetOf at line 72-75 follows it while the race promise is Pending. Program: const { promise, resolve, reject } = Promise.withResolvers(); async function A() { await Promise.race([Bun.file(missing).text(), promise]); } A().then(resolve, reject);. This completes normally: N rejects, the race rejects, A throws, A's return promise rejects, reject settles promise. Walk: N's reaction holds the race reject function. Line 74 sees the race promise Pending with the first-resolving flag clear. The race promise is awaited by A, so line 232 finds A and line 234 appends the frame. Line 236 takes A's return promise RA. RA's reaction is then(resolve, reject); rejectHandlerOf at line 82-83 returns reject. Line 72-75 gives promise. promise is an input of the race, so its reaction again holds the race reject function, which again leads to the race promise and to A. Line 233 only stops at results.size() <…

    Verification: nit — triggered when an async function's return promise is wired (via then(resolve, reject) / catch(reject) with a resolving-function pair) into a promise that the same function is racing against, e.g. a fail-fast worker pool worker().catch(abort) where worker does await Promise.race([nativeOp, abortPromise]). Mechanism, traced in /home/claude/bun/src/jsc/bindings/AsyncStackTrace.cpp:…

Comment thread src/jsc/bindings/AsyncStackTrace.cpp Outdated
…the fixture

The search shared one 32-hop budget between the walk along a reject
function and the fallback walk, so a long chain behind the reject function
could starve the promise that then() returned. Each walk now has its own
limit, the number of walks is limited, and a search that the limit ends
falls back to one walk without reject functions.

A chain that leads back to a generator that is already in the stack is a
cycle. The search now treats it as a dead end, so a fail-fast worker that
races a native promise against its own abort promise gets one frame and
not ten.

On Windows a Bun.file().text() that rejects does not keep the process
alive (#39787), so the fixture printed nothing there. It now uses
Bun.file().stat().
Comment thread src/jsc/bindings/AsyncStackTrace.cpp Outdated
Comment thread src/jsc/bindings/AsyncStackTrace.cpp Outdated
Comment thread src/jsc/bindings/AsyncStackTrace.cpp Outdated
Comment thread src/jsc/bindings/AsyncStackTrace.cpp Outdated
Comment thread src/jsc/bindings/AsyncStackTrace.cpp Outdated
@robobun

robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:32 PM PT - Sep 17th, 2026

✅ @robobun, your commit af78411988ff3095de4a1e01b62a19a3aaf02209 passed in Build #117190! 🎉


🧪   To try this PR locally:

bunx bun-pr 43127

That installs a local version of the PR into your bun-43127 executable, so you can run:

bun-43127 --bun

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

The review finding about repeated frames is confirmed and fixed in 75afa6d.

Shape: worker().catch(abort), where worker() awaits Promise.race([nativePromise, aborted]). The return promise of worker() leads to aborted, and aborted leads back to the race that worker() awaits. On main the stack has one at async worker frame. With the first commit of this PR it had ten.

The search now treats a generator that it already visited as a dead end and tries the fallbacks. The result is the same as on main: one worker frame, then the caller. The fixture shape failFastWorker covers it.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed the updated PR and didn't find any bugs; the per-walk hop budget and cycle guard added in the second commit address the shared-budget concern from my earlier pass. Because the new walk depends on JSC-internal layouts (JSFunctionWithFields resolving-function fields, JSSlimPromiseReaction::handlerOrContext, the capability record's promise slot) that cannot be checked against the vendored WebKit source from this checkout, a human familiar with the JSC promise implementation should still take a look.

What was reviewed:

  • Allocation under AssertNoGC: WTF::HashSet/WTF::Vector use fastMalloc, getConcurrently + getDirect(offset) avoid property-table materialization, and an accessor in the promise slot yields a GetterSetter cell that fails the downcast rather than running user code.
  • Behavior preservation: the FulfillHandler arm is unchanged and the final walkReactions(start, nullptr) pass reduces to the base branch's chain-following, so no shape loses frames when reject-function targets do not pan out.
  • Termination: 32 hops per walk, at most 8 walks plus one plain walk, and seen stops the outer loop on generator cycles; the fixture covers the 40-link chain, 10 chained reject targets, and the worker().catch(abort) cycle.
Extended reasoning...

Overview

The PR extends collectAsyncStackFramesFromPromise in /home/claude/bun/src/jsc/bindings/AsyncStackTrace.cpp so that a native rejection still gets at async frames after JSC has permanently left its promise fast paths (Object.prototype.then defined, Promise.prototype.then replaced, or Promise.prototype frozen). Three new lambdas recognize JSC's own native reject functions stored as reaction handlers and follow them to the promise or await record they would settle; derivedPromiseOf additionally reads the then() result out of a capability record. The single-pass walk became a bounded multi-walk search with fallbacks and a seen set. Tests add a spawned fixture (/home/claude/bun/test/js/bun/util/native-rejection-async-stack-fixture.js) exercising 14 consumption shapes across three triggers, each before/after and with/without AsyncLocalStorage, asserted exactly via toEqual in /home/claude/bun/test/js/bun/util/bun-file.test.ts.

Security risks

None specific: the code only reads engine-internal cells and never invokes user code. I checked the one place user-controlled objects are inspected (derivedPromiseOf reading a promise own property): it uses structure()->getConcurrently and getDirect(offset), so a getter placed there is returned as a GetterSetter cell and fails the JSPromise downcast instead of executing. All field reads are gated on dynamicDowncast.

Level of scrutiny

Moderate-to-high. The change runs under JSC::AssertNoGC, keeps raw JSPromise*/JSAsyncFunctionGenerator* in WTF containers (acceptable only because no JS-heap allocation occurs in the walk, which holds for fastMalloc-backed WTF::Vector/HashSet), and hard-codes assumptions about JSC internals: that calling either resolving function clears ResolvingOther/ResolvingWithInternalMicrotaskOther, that FirstResolvingPromise pairs with isFirstResolvingFunctionCalledFlag, and that the capability record stores its promise as an own data property named promise. This checkout has no vendored WebKit tree, so these could not be verified against source here; if any assumption is wrong the failure mode is benign (the walk hits a non-pending promise or a failed downcast and falls back), but a maintainer who knows the JSC promise rewrite should confirm them. There is no CODEOWNERS entry covering src/jsc/bindings.

Other factors

The earlier inline finding (shared 32-hop budget starving the fallback walk) is addressed: each walkReactions call now has its own hop counter, the driver caps walks at 8, and a final reject-function-free walk preserves the base behavior. The FulfillHandler arm is byte-for-byte the old logic, and the heap-reaction path still only inspects the head of the reaction list, as before. The bug-hunting run exited on a dry streak with no findings. The test asserts exact frame lists (not toContain), drains stdout/stderr concurrently, uses test.concurrent.each, and spawns a fresh process per trigger since the watchpoints cannot be reset; the fixture's main() has no catch, so any thrown error surfaces as non-empty stderr and a nonzero exit, which the assertions catch.

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

For the reviewer who checks the JSC layouts: these are the places in the pinned WebKit (c28156899e) that the walk depends on.

Each read is behind a checked downcast. If a layout changes, the walk finds nothing there and uses the plain then() chain, and the fixture fails.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant