Repository navigation
Conversation
… and cause compareBranch's error arm only matched JSC::ErrorInstance. A DOMException is a WebCore::JSDOMException wrapper whose name and message are prototype accessors, so two DOMExceptions fell through to the toString-tag check and the own-enumerable-property walk, which had nothing to compare: any two DOMExceptions were partially equal. Node takes its error arm for anything that is instanceof Error and compares name, message and cause. Add a DOMException arm ahead of the ErrorInstance arm that requires both sides to be DOMExceptions and reuses errorSubset. Keeping it separate from the ErrorInstance arm keeps DOMException vs Error rejected, as node does.
|
Status Reproduced on main (83d65fa) and release 1.4.0: Fix in Proof: |
|
Warning Review limit reached
Next review available in: 8 minutes Limit details: You’ve used all 5 included reviews currently available under your plan. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan includes up to 5 reviews per rolling hour; 0 remain after this review. WalkthroughThe partial deep equality implementation now handles ChangesDOMException partial equality
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 11:08 PM PT - Aug 15th, 2026
⏳ @robobun, your commit ea227a1 is still building in
|
There was a problem hiding this comment.
LGTM — the new compareBranch arm follows the adjacent URL/KeyObject/Error arms exactly and reuses errorSubset unchanged. Both inline notes describe pre-existing behavior on main (the cause: undefined edge case in errorSubset, and the sibling Bun__deepEquals blind spot in bindings.cpp); neither is introduced or worsened here, so they're fine as follow-ups.
Extended reasoning...
Overview
Adds a 12-line DOMException arm to PartialDeepEqual::compareBranch in src/jsc/modules/NodeUtilTypesModule.cpp, plus a JSDOMException.h include. The arm mirrors the existing URL/KeyObject/Error arms byte-for-byte in shape (inherits<> on both sides → both-or-neither → withCycleGuard + existing helper) and routes to the unmodified errorSubset. The test file gains a 35-case test.each matrix covering name/message/cause differences, subclass, nesting, AbortSignal reasons, mixed-type pairs, own props, and cycle handling — all verified against Node v26 per the PR description, with 14 cases confirmed failing on the unfixed binary.
Security risks
None. This is a pure comparison function in node:assert; no I/O, no allocation beyond what errorSubset already does, no user-controlled paths or shell/network surface. inherits<WebCore::JSDOMException>() is a non-throwing ClassInfo walk, so no new exception-check obligations are introduced before the withCycleGuard call (which handles its own).
Level of scrutiny
Low-to-medium. The native change is mechanical — it copies the exact pattern of the four arms immediately surrounding it, reuses an existing helper without modification, and the placement rationale (ahead of and separate from the ErrorInstance arm so DOMException-vs-Error is rejected, matching Node's tag check) is documented in the code comment and PR body. The test matrix is unusually thorough for a compat fix and pins both the newly-fixed and already-correct behavior.
Other factors
The two inline findings are both pre-existing on main: (1) errorSubset's cause: undefined handling diverges from Node's kPartial for the ErrorInstance arm too — this PR doesn't touch errorSubset; (2) the sibling assert.deepStrictEqual / util.isDeepStrictEqual path in Bun__deepEquals (bindings.cpp) has the same DOMException blind spot — that fix lives in a large shared template also backing Bun.deepEquals and jest matchers, so scoping this PR to compareBranch is reasonable. Neither is a regression; the PR is a strict improvement. The author ran the change under BUN_JSC_validateExceptionChecks=1 and the full test/js/node/assert/ suite plus vendored Node assert tests.
…roperties errorSubset compared `cause` only when expected had it as an own property, and then always compared the values. Node reads cause the same way it reads message, name and errors (an undefined expected value is not compared, an inherited one is), and separately requires an own cause on expected to exist on actual. Move cause into the property loop and keep only the presence check in the own-property block.
There was a problem hiding this comment.
LGTM — both prior review points are addressed (the cause handling in errorSubset now matches Node's structure, and the sibling deepStrictEqual blind spot is documented as deferred to #39308).
What was reviewed:
- The new DOMException arm mirrors the adjacent URL/KeyObject arms exactly;
inherits<>()is non-throwing so no exception check is needed there, and placement ahead of the Error arm preserves the DOMException-vs-Error rejection. - The
errorSubsetrefactor: traced the loop's[[Get]]oncausefor both Error (own non-enumerable data property) and DOMException (own non-enumerable via constructor at JSDOMException.cpp:165) — both are read correctly;vm.propertyNames->causeis a valid CommonIdentifier used elsewhere in bindings. - Test matrix covers the cause edge cases from the earlier review (defined actual vs undefined expected, inherited getters) plus mixed-type pairs, subclasses, nesting, and cycles.
Extended reasoning...
Overview
Two files: src/jsc/modules/NodeUtilTypesModule.cpp (adds a JSDOMException arm to PartialDeepEqual::compareBranch and restructures errorSubset's cause handling) and test/js/node/assert/assert.test.cjs (~50 new test cases across two test.each blocks). Net C++ logic change is ~15 lines.
Security risks
None. This is assert.partialDeepStrictEqual, a Node-compat testing/assertion API. No untrusted-input parsing, no allocation sizing, no auth/crypto surface. The new arm only calls inherits<>() (non-throwing type check) and reuses the existing errorSubset/withCycleGuard machinery.
Level of scrutiny
Low-to-medium. It's native C++ in the JSC bindings, so exception-scope discipline matters — but the change follows the exact pattern of the neighboring URL and KeyObject arms (check both sides' type, require both, delegate to a shared body via withCycleGuard). The errorSubset refactor moves cause into the existing property loop and strips the redundant compare from the own-property block; every get() and getOwnPropertySlot retains its RETURN_IF_EXCEPTION. The PR description states it was run under BUN_JSC_validateExceptionChecks=1.
Other factors
This is my second pass. In the first, I flagged (a) the sibling Bun__deepEquals blind spot — author responded it's a separate implementation handled in #39308 and updated the PR description accordingly, which satisfies REVIEW.md's "if a site is intentionally excluded, say so"; and (b) a pre-existing cause: undefined divergence in errorSubset — author fixed it in d8dc16c and added the error cause test block (3 cases fail on the unfixed binary per the description). The comment-cop nag about the multi-line arm comment was also addressed (collapsed to one line). All threads are resolved.
Test coverage is thorough: name/message/cause variations, empty message leniency, subclass instances, nested in objects/arrays, mixed DOMException-vs-Error/plain-object pairs in both directions, own enumerable extras, AbortSignal.abort().reason, self-referencing causes, and same-instance identity. Assertions use toThrow(assert.AssertionError) (specific class, not bare toThrow()). The description states 14 DOMException cases and 3 cause cases fail under USE_SYSTEM_BUN=1 and all 58 pass with the fix, plus the vendored Node assert suites (486 pass).
I verified vm.propertyNames->cause is a real CommonIdentifier (used in bindings.cpp, ErrorCode.cpp, JSDOMException.cpp) and that Bun's DOMException constructor stores cause as an own non-enumerable property (JSDOMException.cpp:165), so the errorSubset get() and getOwnPropertySlot paths both see it as intended.
Problem
assert.partialDeepStrictEqual(new DOMException("boom", "AbortError"), new DOMException("other", "NotFoundError"))passes. Node v26 throwsERR_ASSERTION. Same for two DOMExceptions that differ only in message, only in name, or only incause, and for an expected DOMException that has acausewhen the actual one has none. The same holds when the DOMExceptions are nested in objects or arrays, and forAbortSignalreasons compared against a DOMException with a different name or message.assert.partialDeepStrictEqualis native (PartialDeepEqual::compareBranch,src/jsc/modules/NodeUtilTypesModule.cpp). Its error arm keys oninherits<JSC::ErrorInstance>(). A DOMException is aWebCore::JSDOMExceptionwrapper, not anErrorInstance, so the pair falls through to the generic tail ofcompareBranch: theObject.prototype.toStringtags match ([object DOMException]on both sides) andobjectSubsetwalks expected's own enumerable properties, of which a DOMException has none (nameandmessageare accessors onDOMException.prototype,causeis an own non-enumerable property). Nothing is compared, so every pair is reported as equal.errorSubset, shared by the Error arm and the new DOMException arm):causewas only looked at when expected had it as an own property, and was then always compared. SopartialDeepStrictEqual(new Error("x", { cause: 1 }), new Error("x", { cause: undefined }))threw (node passes) and an expected error whosecausecomes from a prototype getter was never compared (node compares it).Fix
compareBranch, next to the URL and KeyObject arms: if either side is aJSDOMException, both must be, and the pair goes through the existingerrorSubset.errorSubsetis the right comparison because it reads the error properties with an ordinary[[Get]], which runs the DOMException prototype accessors, and applies node's own-causerule. That is what node's error arm does: node treats anythinginstanceof Erroras an error there and reads the same properties, with the same partial-mode leniency (anundefinedexpected value or an empty expected message is ignored). DOMException's prototype chain goes throughError.prototype, so node takes that arm for it.ErrorInstanceone, rather than a widening of theErrorInstancecheck: node rejects a DOMException against a plain Error with the same name and message (its tag check runs before its error arm), anderrorSubsetalone would accept that pair. Requiring both sides to be DOMExceptions keeps that rejection, and the ordering keeps it even ifJSDOMExceptionlater becomes anErrorInstance(DOMException: inherit from ErrorInstance for [[ErrorData]] and .stack #32898).errorSubset, movecauseinto the loop that readsmessage,nameanderrors, and leave only the presence check in the own-property block. That is node's structure exactly:isEnumerableOrIdenticalfor each of the four properties, then the separate own-causepresence rule (comparisons.js). It fixes the twocausedivergences above for plain Errors as well as DOMExceptions.assert.deepStrictEqual/util.isDeepStrictEqualhave the same DOMException blind spot inBun__deepEquals(bindings.cpp), a different implementation that also backsBun.deepEqualsand theexpect()matchers. That is fixed separately in deepEquals: compare DOMException name, message and cause #39308; the two changes do not overlap.test/js/node/assert/assert.test.cjs, two new blocks in thepartialDeepStrictEqualdescribe.DOMException: 14 of its cases fail on the unfixed binary (USE_SYSTEM_BUN=1, every "rejects" case whose operands are both DOMExceptions); the accepted cases and the mixed pairs (DOMException vs{}, vs a same-shaped plain object, vs an Error in both directions, expected with an extra own property) pin the behavior that was already correct.error cause: 3 of its cases fail on the unfixed binary (undefined expected cause, and the two inherited-cause cases); the rest pin the existing cause rules. All 58 tests in the file pass with the fix. Every expectation was checked against Node v26.3.0.test/js/node/assert/plus the vendoredtest-assert.js,test-assert-class.jsandtest-assert-deep-with-error.js(486 pass),test-assert-typedarray-deepequal.jsandtest-util-isDeepStrictEqual.js, and the test file underBUN_JSC_validateExceptionChecks=1.Background
assert.partialDeepStrictEqual(actual, expected)is node's "partial" deep equality:expectedhas to be recursively contained inactual(own enumerable properties of expected must exist and match on actual, arrays are matched as an in-order subsequence, and so on). Bun implements it natively incompareBranch, which is a chain of arms, one per kind of value (Map, typed array, KeyObject, URL, Error, Date, ...), each of which first checks that both sides are of that kind and then compares the kind's internal state before falling back to the own-property subset walk. A pair that matches no arm ends in theObject.prototype.toStringtag check plus that walk.errorSubsetis the body of the Error arm. Node's rules for errors in partial mode, which it implements:message,name,causeanderrorsare read with[[Get]]on both sides and compared unless the expected value isundefined(or, formessage, empty); an owncauseon expected, even one set toundefined, requires an owncauseon actual; then the normal own-property subset walk runs.JSDOMExceptionis the JS wrapper for WebCore'sDOMException. Its prototype's prototype isError.prototype, soinstanceof Erroris true, but the object itself is a plain DOM wrapper:name/message/codeare prototype getters over the wrapped C++ object, and the constructor stores an optionscauseas an own non-enumerable property, the same shape node's DOMException has.Comparison table, fixed debug build vs Node v26.3.0
Each line is
assert.partialDeepStrictEqual(actual, expected). The first ten lines pass on the unfixed binary; everything else was already correct.Bun with the fix and node agree on every line.