Skip to content

assert.fail: match Node 26 single-argument behavior - #33567

Open
robobun wants to merge 7 commits into
mainfrom
farm/9ccd746e/assert-fail-node26
Open

robobun wants to merge 7 commits into
mainfrom
farm/9ccd746e/assert-fail-node26

Conversation

@robobun

@robobun robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

Node 26 removed the end-of-life DEP0094 multi-argument behaviour of assert.fail. Bun still implemented the legacy form, so every observable property of the thrown AssertionError diverged for calls with 2+ arguments, and Bun emitted a DeprecationWarning Node no longer emits.

Repro

import assert from "node:assert";
const row = (label, fn) => { try { fn(); } catch (e) {
  console.log(label, JSON.stringify({ message: e.message, actual: e.actual, expected: e.expected,
                                      operator: e.operator, generatedMessage: e.generatedMessage })); } };
process.on("warning", w => console.log("WARNING", w.name, w.code ?? ""));
assert.fail(1, 2);
assert.fail(1, 2, undefined, "==");
assert.fail("a", "b", "m");

Node v26.3.0 (first arg is the message, rest ignored, no warning):

fail(1,2)              {"message":"1","operator":"fail","generatedMessage":false}
fail(1,2,undef,'==')   {"message":"1","operator":"fail","generatedMessage":false}
fail('a','b','m')      {"message":"a","operator":"fail","generatedMessage":false}

Bun before this change (legacy multi-arg form):

fail(1,2)              {"message":"1 != 2","actual":1,"expected":2,"operator":"!=","generatedMessage":true}
fail(1,2,undef,'==')   {"message":"1 == 2","actual":1,"expected":2,"operator":"==","generatedMessage":true}
fail('a','b','m')      {"message":"m","actual":"a","expected":"b","operator":"fail","generatedMessage":false}
WARNING DeprecationWarning DEP0094

Cause

fail in src/js/node/assert.ts kept the deprecated branch: it synthesized "actual operator expected" messages, set actual/expected/operator from the extra arguments, and called process.emitWarning(..., "DEP0094").

Fix

Mirror Node 26: fail(message) uses only the first argument. If it is an Error it is thrown; otherwise it becomes the message. operator is always "fail", actual/expected are undefined, and generatedMessage is true only when no argument is given. The legacy multi-argument synthesis and the DEP0094 warning are removed.

Verification

Added tests in test/js/node/assert/assert.spec.ts. They pass with the fix and fail on the released build (legacy actual/expected/operator and the DEP0094 warning).

@github-actions github-actions Bot added the claude label Jul 6, 2026
@robobun

robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:05 PM PT - Jul 6th, 2026

❌ @robobun, your commit b0e29b3 has some failures in Build #69475 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 33567

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

bun-33567 --bun

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d53bcca9-9798-4e5d-8286-ad435d7c7e0c

📥 Commits

Reviewing files that changed from the base of the PR and between 246348d and b0e29b3.

📒 Files selected for processing (3)
  • src/js/node/assert.ts
  • test/js/node/assert/assert-fail.test.ts
  • test/js/node/test/parallel/test-assert-fail-deprecation.js

Walkthrough

This PR simplifies Node.js's assert.fail implementation in Bun by removing the deprecated multi-argument overload and its associated deprecation-warning state, reducing it to a single optional message parameter. A new test file validates the updated behavior, including error throwing, message handling, and warning suppression.

Changes

assert.fail Simplification

Layer / File(s) Summary
Remove deprecated overload and warning logic
src/js/node/assert.ts
Removes the warned deprecation-warning flag and replaces the multi-argument fail(actual, expected, message?, operator?, stackStartFn?) implementation with a single `fail(message?: string
Tests for updated fail() behavior
test/js/node/assert/assert-fail.test.ts
Adds a new test suite verifying default/custom messages, ignored extra arguments, direct Error throwing, and suppression of the DEP0094 deprecation warning via a spawned process check.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant fail as assert.fail
  participant AssertionError

  Caller->>fail: fail(message?)
  alt message is Error
    fail->>Caller: throw message
  else message is string or undefined
    fail->>AssertionError: new AssertionError({actual: undefined, expected: undefined, operator: "fail", stackStartFn: fail})
    AssertionError-->>fail: error instance
    fail->>Caller: throw error
  end
Loading

Related PRs: None identified.

Suggested labels: node:assert, semver-major, needs-tests

Suggested reviewers: None identified.

Poem
A rabbit hopped through assert's old code,
Trimmed the warnings, lightened the load.
One message now, no more, no less,
fail throws clean, without the mess.
Tests confirm it, hop by hop —
DEP0094's warning? Made to stop. 🐇

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: assert.fail now matches Node 26 single-argument behavior.
Description check ✅ Passed The description covers what changed and how it was verified, though the template headings are paraphrased rather than exact.
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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/node/assert.ts (1)

120-123: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Force generatedMessage to reflect argument presence, not message truthiness.

AssertionError currently sets generatedMessage = !message, so explicit falsy messages like "", 0, or false still become generated. That violates the new contract that only zero-argument assert.fail() is generated.

🐛 Proposed fix
-  const err = new AssertionError(errArgs);
-  if (internalMessage) {
-    err.generatedMessage = true;
-  }
+  const err = new AssertionError(errArgs);
+  err.generatedMessage = internalMessage;
🤖 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 `@src/js/node/assert.ts` around lines 120 - 123, The generatedMessage flag in
AssertionError creation should depend on whether a message argument was actually
provided, not on the message value’s truthiness. Update the assert path around
AssertionError and the internalMessage check in assert.ts so generatedMessage is
set only for zero-argument fail/assert cases, while explicit falsy messages like
empty string, 0, or false remain non-generated.
🤖 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 `@src/js/node/assert.ts`:
- Line 110: Update the message-type checks in assert.ts so they use
Error.isError instead of instanceof Error in the assert.fail and innerFail
paths, since instanceof misses cross-realm errors. Make sure the logic still
rethrows real Error objects directly, and add coverage for a node:vm-created
error case so assert.fail(vmError) is handled correctly.

In `@test/js/node/assert/assert-fail.test.ts`:
- Around line 1-93: This Bun-only assert.fail coverage should be moved into the
existing assert test suite instead of living in a standalone file. Relocate the
cases into the current assert tests alongside the other assert.fail coverage,
keeping the same capture helper and the assert.fail-related checks so the
behavior remains discoverable in the main suite. Use the existing assert test
file and the assert.fail symbol to place these new expectations where the rest
of the assert API tests already live.
- Around line 68-70: Tighten the `assert.fail` test so it verifies object
identity rather than matcher behavior: in the `throws the first argument when it
is an Error` case, catch the thrown value from `assert.fail(err)` and assert it
is the exact same `Error` instance with `toBe(err)`. Keep the change localized
to the `assert-fail.test.ts` test block using the existing `assert.fail` and
`err` symbols.
- Around line 79-89: The test in assert-fail.test.ts is using a broad stderr
sentinel by asserting that stderr does not contain “WARNING”, which can fail on
unrelated benign output. Update the warning-checking logic around the
assert.fail(1, 2) process case to use a more specific sentinel for process
warnings (or rely on the nonzero exit behavior together with the existing
DEP0094 assertion), and keep the assertion tied to the relevant warning path
rather than generic stderr content.

---

Outside diff comments:
In `@src/js/node/assert.ts`:
- Around line 120-123: The generatedMessage flag in AssertionError creation
should depend on whether a message argument was actually provided, not on the
message value’s truthiness. Update the assert path around AssertionError and the
internalMessage check in assert.ts so generatedMessage is set only for
zero-argument fail/assert cases, while explicit falsy messages like empty
string, 0, or false remain non-generated.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 52faf149-877c-4447-b35a-8f65ac4cedad

📥 Commits

Reviewing files that changed from the base of the PR and between a12475e and 246348d.

📒 Files selected for processing (2)
  • src/js/node/assert.ts
  • test/js/node/assert/assert-fail.test.ts

Comment thread src/js/node/assert.ts Outdated
Comment thread test/js/node/assert/assert-fail.test.ts
Comment thread test/js/node/assert/assert-fail.test.ts Outdated
Comment thread test/js/node/assert/assert-fail.test.ts Outdated
Comment thread src/js/node/assert.ts
Comment thread src/js/node/assert.ts
Comment thread src/js/node/assert.ts

@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.

LGTM — all prior feedback addressed; implementation now matches Node 26's assert.fail exactly.

Extended reasoning...

Overview

This PR updates assert.fail in src/js/node/assert.ts to match Node 26's removal of the end-of-life DEP0094 multi-argument form. The function now takes a single optional message argument: if it's an Error (checked via cross-realm-safe Error.isError), it's thrown directly; otherwise it becomes the AssertionError message with operator: 'fail' and actual/expected undefined. The legacy warned flag, process.emitWarning('DEP0094') call, and multi-argument message synthesis are removed. innerFail was also updated to use Error.isError. A new test file covers the full contract, and the now-obsolete vendored test-assert-fail-deprecation.js is deleted.

Security risks

None. Pure JavaScript, no native code, no I/O, no auth/crypto/permissions surface. The change only affects error-object construction in node:assert.

Level of scrutiny

Moderate — this is a user-facing behavioral change to a Node.js compat API, but it's a straightforward alignment with an upstream Node semver-major removal (DEP0094 went end-of-life). The final implementation is a near-verbatim port of Node's lib/assert.js fail().

Other factors

This PR went through three rounds of my review feedback plus CodeRabbit's, and all issues are now resolved and verified in the current diff:

  • message === undefined check (not arguments.length) so assert.fail(undefined) yields 'Failed' — fixed and tested.
  • Conditional if (internalMessage) err.generatedMessage = true restored so falsy messages ('', 0, null) keep generatedMessage: true per the AssertionError constructor — fixed and tested.
  • Vendored test-assert-fail-deprecation.js deleted so CI doesn't break — done.
  • Error.isError for cross-realm errors, identity-preserving Error throw, unique stderr sentinel — all applied.

The new test file has comprehensive coverage (no-arg, explicit undefined, empty string, string message, multi-arg-ignored, Error identity, cross-realm Error, no-DEP0094-warning subprocess check) and follows harness conventions. No outstanding reviewer comments remain.

@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.

LGTM — all prior feedback addressed; fail() now mirrors Node 26 exactly.

Extended reasoning...

Overview

This PR removes the end-of-life DEP0094 multi-argument form of assert.fail from src/js/node/assert.ts, replacing it with the Node 26 single-argument implementation: Error.isError(message) → rethrow; message === undefined → default "Failed"; fixed operator: "fail", actual/expected: undefined; conditional if (internalMessage) err.generatedMessage = true. It also switches innerFail's Error check to Error.isError, adds a focused test file test/js/node/assert/assert-fail.test.ts, and deletes the vendored test-assert-fail-deprecation.js (removed upstream when DEP0094 went EOL).

Security risks

None. This is a pure Node-compat behavioral sync in the assert module — no auth, crypto, filesystem, or network paths are touched. No untrusted input parsing.

Level of scrutiny

Low-to-moderate. The runtime change is ~15 lines that now match Node's lib/assert.js line-for-line. It went through three rounds of review here (the arguments.length vs === undefined regression, the stale vendored deprecation test, and the unconditional generatedMessage overwrite for falsy messages) — all fixed with regression tests. CodeRabbit's cross-realm Error.isError, identity assertion, and stderr-sentinel feedback were also applied.

Other factors

Test coverage is thorough: no-arg, explicit undefined, empty-string (falsy → generatedMessage: true), string message, same-realm Error identity, cross-realm vm Error identity, multi-arg ignored (three variants), and a spawned subprocess asserting no DEP0094 warning. The remaining vendored test-assert-fail.js only exercises single-arg forms, which remain correct. No outstanding reviewer comments; all threads resolved. The bug hunter found nothing on the latest revision.

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Diff is green. All 257 test jobs that ran passed, including the new test/js/node/assert/assert-fail.test.ts (8/8) on every lane.

The only CI failure on build 69475 is unrelated infra: the :darwin: 26 aarch64 - test-bun lane timed out downloading the build artifact (buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun') before running any tests. The 8 expired + 20 waiting_failed jobs are all downstream of that.

This change is JS-only (src/js/node/assert.ts + tests) and cannot affect artifact download. Ready for a maintainer to re-run the flaked darwin lane or merge.

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