Repository navigation
Conversation
… with no own `cause` The five places that create the AggregateError of Promise.any, and JSPromise::resolvePromise for a promise that is resolved with itself, pass jsUndefined() as the cause. ErrorInstance::finishCreation defines `cause` for every value that is not empty, so each of these errors has an own `cause: undefined`. ECMA-262 defines only `errors` on the AggregateError of Promise.any, and no `cause` on the TypeError of a promise resolved with itself. Pass the empty JSValue. That is the value every other native caller of ErrorInstance::create passes when there is no cause.
|
Preview build of f4adc68: |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it is an observable behavior change on a spec-facing surface (Promise.any and self-resolution errors) in paths that CODEOWNERS routes to the JSC reviewers, a human look is still worthwhile.
What was reviewed:
- Traced the empty JSValue through createAggregateError -> ErrorInstance::create -> finishCreation: the only read of
causeis theisEmpty()guard, so the sentinel is safe on both finishCreation overloads. - Searched the three source files and the rest of runtime/ for any other spec-internal error still passing
jsUndefined()as cause — none remain; the other native callers already pass the empty JSValue. - Checked the new stress test against JSTests/README.md conventions: it is pure JS in JSTests/stress, fails only by throwing, prints nothing, and the explicit
{ cause: undefined }control keeps the constructor path's own-property behavior pinned.
Extended reasoning...
The change replaces jsUndefined() with the empty JSValue() as the cause argument at six error-creation sites in JSMicrotask.cpp, JSPromise.cpp and JSPromiseConstructor.cpp (the five Promise.any AggregateError paths and the self-resolution TypeError), and adds one new stress test; no headers, no #if, no security-sensitive surface. The sentinel is consumed only by the !cause.isEmpty() guard in ErrorInstance::finishCreation, and the diff matches the pattern of the existing native callers in SuppressedError.cpp and NullSetterFunction.cpp. The bug hunt ran dry with no findings and the five ruled-out candidates all concerned downstream Bun consumers or test labeling rather than the engine change. Deferred rather than approved because /Source/JavaScriptCore and /JSTests are covered by the repository's CODEOWNERS file and the change alters observable JS semantics ("cause" in error, enumerability of a later error.cause = x), which a maintainer should sign off on.
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Promise error construction Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp, Source/JavaScriptCore/runtime/JSMicrotask.cpp, Source/JavaScriptCore/runtime/JSPromise.cpp |
Promise.any AggregateError creation paths and self-resolution TypeError creation now pass an empty JSValue instead of jsUndefined(). |
Stress test coverage JSTests/stress/promise-errors-have-no-own-cause.js |
The new test checks Promise.any rejection errors, self-resolution errors, AggregateError.errors descriptors and lengths, and whether explicit cause: undefined creates an own cause property. |
Suggested reviewers: constellation
Priority:
Merge Risk: 
Minimal · up to f4adc
-
No concrete current-head regression or unresolved merge-blocking risk was identified.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description is detailed and relevant. It explains the problem, fix, affected call sites, behavior change, testing, performance impact, and upstream relationship. It does not include the template's explicit Bugzilla URL, Reviewed by NOBODY line, or a standalone bug-title heading, but the required technical context is substantially complete.
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
Supersedes #340.
Problem
AggregateErrorofPromise.anyand theTypeErrorof a promise resolved with itself have an owncause: undefined.Object.hasOwn(error, "cause")istrue, andutil.inspectprints[cause]: undefined. Node and Bun 1.3.5 have no owncause.jsUndefined()as the cause:JSMicrotask.cpp:528,JSPromiseConstructor.cpp:1227,:1337,:1374,:1409,JSPromise.cpp:750.ErrorInstance::finishCreationdefinescausefor every non-empty value.Fix
JSValue. Nothing else changes.errorson thisAggregateError. The other 17 native callers pass the empty value.JSTests/stress/promise-errors-have-no-own-cause.jsexits 3 before, 0 after. Bun built against this branch passes 8 new rows that fail on Bun 1.4.3.Background
ErrorInstance::createandcreateAggregateErrortake the cause by position. The emptyJSValuemeans no cause.undefinedis a real cause:new Error(m, { cause: undefined })must define one.Promise.anycreates its error in five functions: a microtask job, the synchronous path, a reject element function, and two subclass variants.Promise.any-only creator with V8's message (Promise.any: give the AggregateError a stack, message, and no spurious cause #340): ECMA-262 sets no message. Considered a signature that rejects an undefined cause: it rewrites upstream's callers, so it belongs upstream.Downsides
"cause" in errorisfalseanderror.cause = xis enumerable, as in node.message, and nostackwhen no JS frame exists (An error without stack frames has its header as itsstackbun#45883).Notes
Before and after.
Promise.any([Promise.reject(new Error("r1")), Promise.reject("r2")]), read in acatch:Object.hasOwn(e, "cause")util.inspect(e)has[cause]: undefinede.cause = 1is enumerablestack,message,errorsfalseerrors,line,column,stackfalsecause,errorstrueerrorsfalseMeasurements.
causediffers from node v26.3.0 in 0 of 7 producer rows (was 7 of 7 on Bun 1.4.3-canary.1+367d939d9). Constructor controls changed: 0 of 2.JSTests/stress/promise-errors-have-no-own-cause.js: exit 3 on the basejsc(autobuild-680ef47c,main), exit 0 on thejscof this pull request (autobuild-preview-pr-895-f4adc68d), both linux-amd64-lto. Sites with an own cause: 6 of 6 -> 0 of 6. The debug builds give the same two exit codes (prebuilt debugjscof 5e3554f, and a local debug build of this branch).jscdebug 5e3554f).Promise.anyrejection: Array +1.00, Error +1.00 (base +1.00 / +1.00). Own properties 2 -> 1.#if. 1 new JSTests file (80 lines). 0 upstream tests edited.Promise.any: 4747.8 -> 4594.2 (delta -153.5). Per fulfilledPromise.any: delta +0.8, which is inside the spread. Method: count ofinstructions:u, slope from N = 100000 to N = 200000 calls, median of 5 runs,jsc --useConcurrentJIT=0, linux-amd64-lto, spread at most 4.9 per call.perfis not installed on the machine that ran this. A 20-lineperf_event_openwrapper made the count.jsctext: x64 48,084,159 -> 48,084,127 bytes (delta -32), arm64 delta 0 (llvm-size, lto lanes,main680ef47 against this pull request).built-ins/Promiseandbuilt-ins/AggregateError(757 files, 1330 runs) have 0 failures on the basejsc, 0 on this pull request, 0 new. A small script ran them. It followsTools/Scripts/test262/runner.py: the same scenarios and flags, and a run passes whenjscexits 0. The full suite is the CItestjob: it passes on the three tested lanes (linux-amd64-lto, linux-amd64-lto-hardened, linux-arm64-lto).Cost. Nothing is added. Each edited call is behind an existing guard:
if (!count)in thePromise.anyfunctions,if (resolution == this) [[unlikely]]inresolvePromise. OneputDirectper error goes away.Row to site. Each row of the test is named after the function that creates the error.
Promise.any([Promise.reject(1), Promise.reject(2)])promiseAnyResolveJob,JSMicrotask.cpp:528Promise.any([])promiseConstructorFuncAny,JSPromiseConstructor.cpp:1337Promise.any([p]),pa rejected promise with an ownthenpromiseAnyRejectFunction,JSPromiseConstructor.cpp:1374DerivedPromise.any([])promiseAnySlow,JSPromiseConstructor.cpp:1227DerivedPromise.any([DerivedPromise.reject(1)])promiseAnySlowRejectFunction,JSPromiseConstructor.cpp:1409JSPromise::resolvePromise,JSPromise.cpp:750const p = Promise.resolve().then(() => p)JSPromise::resolvePromise,JSPromise.cpp:750Why the fix is at the call sites.
finishCreationcannot drop an undefined cause: the constructor path gives it the result ofgetIfPropertyExists(options, "cause"), andnew Error(m, { cause: undefined })must owncause. The test keeps that as a control. No code of Bun runs between the creation of these errors and their delivery to a handler.Not changed, and why.
messageof thePromise.anyerror stays empty. ECMA-262 sets none. V8 sets "All promises were rejected". That text is a separate decision for the fork.stackstaysundefinedon an error that is created while no JS frame is on the stack. That is the ruling in An error without stack frames has its header as itsstackbun#45883 and [JSC] An error whose captured stack has no frames gets its stack from the host's hook #829.cause: undefinedare in Do not define an owncauseon native errors that have none bun#44411.AggregateErroris print AggregateError more distinctly bun#21528.Relation to #340. This takes the empty
JSValueas the cause at the fivePromise.anysites from #340. It does not take the creator function, the message, or the change toErrorInstance::materializeErrorInfoIfNeeded(the same change as #829, which is closed). #340 did not changeJSPromise.cpp:750.Upstream. WebKit
mainhas the same six lines today (JSMicrotask.cpp:500,JSPromiseConstructor.cpp:1210,:1320,:1357,:1392,JSPromise.cpp:558). The patch and the test have noUSE(BUN_JSC_ADDITIONS)and nothing specific to Bun, so upstream can take both as they are.Rows for Bun. This is the test for the WebKit bump in oven-sh/bun that first includes this change (
test/js/bun/jsc/webkit-upgrade-<sha>.test.ts). Run on a debug build of Bunmain5e182948be against the preview build of this pull request (bun bd --webkit-version=autobuild-preview-pr-895-f4adc68d test <file>): 9 pass. Run on Bun 1.4.3-canary.1+367d939d9: 8 fail, 1 pass (the control).Suites run on that debug build of Bun
mainagainst the preview build: the rows above (9 pass),test/js/bun/jsc/webkit-upgrade-f59345e04a.test.ts(5 pass, it has a self-resolution row),test/js/bun/util/inspect-error.test.js(45 pass),test/js/node/v8/capture-stack-trace.test.js(71 pass),test/js/node/promise/reject-tostring.test.ts(1 pass) and the error block oftest/js/bun/module-graph/module-graph.test.ts(9 pass). A local debug build of this branch (bun run build:local) gives the same results, with one difference: the self-resolution row of the upgrade test takes 5.3 s on the unoptimized local JavaScriptCore, which is above the default 5 s timeout. It passes there with a longer timeout.