Repository navigation
Conversation
Collaborator
Author
|
Updated 2:08 PM PT - Oct 18th, 2026
✅ @robobun, your commit 34cd03a2f157a398c9ad98a29d991b9926c69a39 passed in 🧪 To try this PR locally: bunx bun-pr 44491That installs a local version of the PR into your bun-44491 --bun |
Collaborator
Author
|
Status Reproduced on 1.4.3-canary.1 and on main, release builds: // bun test ./green.test.js
import { test, expect } from "bun:test";
class E extends Error { get message() { throw new TypeError("a bug in the getter"); } }
let n = 0;
test("flaky", () => { if (n++ === 0) throw new E(); }, { retry: 2 });
test("must fail", () => { console.log("BODY RAN"); expect(1).toBe(2); });
The PR is a draft: its self-review did not complete. The body lists the checks that were done by hand. |
…next callback The runner called each hook, test body and describe body through EventLoop::run_callback_with_result_and_forcefully_drain_microtasks. With an exception pending that helper did not call the callback and returned Ok(undefined), which the runner read as the callback's own return: the entry was reported as a pass without running. The runner now takes such an exception in three places: after each of its prints of an error (the printer runs the error's own accessors), before each callback, and at the end of each file. The helper had one caller. It is deleted and the call is made directly.
…e, without a discarded take
robobun
force-pushed
the
robobun/54dc25df/test-runner-pending-exception
branch
from
October 17, 2026 07:46
6b4ebfe to
bfbec5b
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
bun testprints(pass)for hooks, tests anddescribebodies it never called, once native code returns with a JS exception still pending. Withretry, a failing run exits 0. Debug builds abort:panic: a task returned Ok with a JS exception pending.EventLoop::run_callback_with_result_and_forcefully_drain_microtasks(src/jsc/event_loop.rs:1262) does not call over a pending exception and returnsOk(undefined).BunTest::run_test_callback, its only caller, reads that as the callback's return.messagegetter throws while bun prints it.Fix
test/js/bun/test/failure-skip.test.ts(11 new tests fail on main),expect-extend.test.js. Alsotest/js/bun/testandtest/cli/test.Background
Err(Thrown)from the helper: a never-calledtest.failingprints(pass). Considered a check after prints only: aBun.serveexception still skips callbacks.Downsides
# Unhandled error between tests. A run that exited 0 with skipped tests can exit 1..textis unchanged: 58,187,957 bytes.Notes
Repros. Release builds. 1.4.3-canary.1 and main give the same results.
(fail) first,(pass) second,(pass) third,2 pass 1 fail. OnlybeforeEach ranandfirst body ranprint.0 pass 3 fail.(pass) flaky (attempt 2),(pass) must fail,2 pass 0 fail, exit 0.BODY RANnever prints.1 pass 1 fail, exit 1.Other results on main:
describebody that follows one that throws such an error is not called. Its tests are not collected.--isolatethe process aborts.z.literal("expected").parse(cyclic)withcyclic.self = cyclic. Themessagegetter ofZodErrorcallsJSON.stringifyon the issues. The next test prints(pass)and its body never runs.Bun.servewith afetchhandler that returns a value that is not a Response, and whose inspect hook throws. The response path of the server (render_missing_invalid_response) leaves the exception, not a print of the runner. A file with one failing test gives2 pass 0 fail, exit 0. The next(done) =>test getsdone === undefined.The three places.
print_errorinbun_test.rswraps the threerun_error_handlercalls of the runner (BunTest::on_uncaught_exception,on_unhandled_rejectionwith no active file, a rejected async matcher inexpect.rs). The exception prints under the same report. If that second print throws again, that exception is taken and onenote:line replaces it. A termination is left forBunTest::run.run_test_callback, above the bind ofdone. Without that order the bind meets the exception, charges it to the entry, and calls the body withdoneundefined. The exception is reported as# Unhandled error between tests.TestCommand::runbeforehandle_rejected_promises, throughreport_error_or_terminate. That also runs the microtask checkpoint that the pending exception held back.Measurements. Release builds of f4d755a with and without this diff. Instruction counts are gdb
stepicounts inside the named functions. The numbers predate a later 2-line change in the cold functionprint_exception_left_by_print.run_test_callback, the helper,drain_microtasks_with_globalanddrain_quic_if_necessary. 10 identical callbacks.run_callback/run_callback_with_result: 661 / 690 bytes on both builds. The disassembly is equal except one relocated data address..textis 58,187,957 bytes and the stripped binary is 80,873,032 bytes on both builds. Function symbols: helper -911 B,run_test_callback+263 B,on_uncaught_exception643 B to 311 B plus 576 B (print_uncaught_exception), three cold functions +334 B,TestCommand::run+80 B,execute_custom_matcher+127 B. 0 host functions.run_error_handler50 vs 50 calls,write98 vs 98 syscalls.writesyscalls 49 vs 49. The three new cold paths run 0 times.run_error_handlercall, +1write. hooks.test.js: 10 of 10 callbacks called (main 2 of 10). Per file: +3 instructions.test/js/bun/test,test/cli/testandtest/js/node/test_runner. 104 files pass.pretty-format-overflow.test.tsandsnapshot-tests/snapshots/snapshot.test.tsfail the same way on main.--isolateand--parallel): 180 of 180 release runs and 54 of 54 debug runs give the callback order, verdict lines and pass/fail counts of the control. 70 and 21 of them (theBun.servesource) add1 error. One fixture where every test passes goes from exit 0 to exit 1. Debug aborts: 0 of 16 (main 9 of 16). JUnit: 3 testcase / 3 failure (control 3 / 3). GitHub annotations: one more::errorline per exception.Tests.
messagegetter that throws (left by a print of the runner) and theBun.servepath (left outside the runner). error printer: clear a throwing error.message getter's exception before the next property read #38586, error printer: clear the pending exception when formatting a non-Error uncaught value throws #36921, error printer: print a thrown object that arrives in a JSC::Exception #43238 and Bun.serve: fix crash and cross-request 500 when inspecting a non-Response return value throws #43058 fix these sources. After they merge, the tests still pass, but they no longer reach the new code.Bun.servesource they do not assert the error count, because a fix in the server removes it.asyncthrow,done(err)) hits a debug assertion inside the printer (Unexpected exception observed,assertNoExceptionExceptTermination), on main and on this branch. error printer: clear a throwing error.message getter's exception before the next property read #38586 changes that site. The tests do not use those paths.describebody as the first callback after an exception from outside the runner. On a debug buildtick()asserts before collection starts. The code is the same first statement ofrun_test_callback.print_errorcall inon_unhandled_rejectionwith no active file.Self-review. The review run stopped before its verdict. Checks done by hand on its open points:
print:tests and theexpect-extendtest fail. Without the take before a callback,server: the next callback runsfails. Without the take at file end, bothserver: the next file runstests fail.BUN_JSC_validateExceptionChecks=1.has_exception(), asenter_jsandrelease_task_unrundo.fetch()client in place of the raw socket, a debug build of this branch gives the control plus1 error(3 of 3 runs). On 1.4.3-canary.1 the same file is3 pass 0 fail, exit 0, in 2 of 3 runs.Left as is.
run_callbackandrun_callback_with_resultstill skip a callback over a pending exception. Both can run inside a host call, where the exception has an owner.JSValue::callreturnsOk(undefined)without a call for a disposedBun.ModuleGraphcontext or a retired--isolaterealm.Bun__JSValue__calldoes the same for a stopping VM. The runner cannot tell that from a return.tick_turnreturns with the exception in release and asserts in debug. A fold there changesbun run.handle_top_level_test_error_before_javascript_startexits 1 with no message.run_test_callback: bun test: attribute late expect() calls of an abandoned test to that test instead of the next one #38880, mock.module: resolve the specifier when tail-called from a runner-invoked callback #38034, bun test: run test callbacks with the event loop entered #43356.