diff --git a/src/runtime/test_runner/jest.rs b/src/runtime/test_runner/jest.rs index 2889638ff424..abc3ddca651a 100644 --- a/src/runtime/test_runner/jest.rs +++ b/src/runtime/test_runner/jest.rs @@ -649,7 +649,25 @@ pub(crate) mod on_unhandled_rejection { let exception_list = jsc_vm .on_unhandled_rejection_exception_list .map(|p| unsafe { &mut *p.as_ptr() }); + + // No active test: the rejection escaped every test/hook (e.g. a file's + // top-level async IIFE). Report it like an uncaught exception between + // tests so the file fails instead of being silently swallowed + // (issues/34859). + if let Some(runner) = Jest::runner() { + runner.unhandled_errors_between_tests += 1; + bun_core::pretty_errorln!( + "\n# Unhandled error between tests\n-------------------------------\n", + ); + Output::flush(); + } + jsc_vm.run_error_handler(rejection, exception_list); + + if let Some(runner) = Jest::runner() { + bun_core::pretty_error!("-------------------------------\n\n"); + Output::flush(); + } } } diff --git a/test/cli/test/rejections-between-tests.test.ts b/test/cli/test/rejections-between-tests.test.ts new file mode 100644 index 000000000000..d98ba6666f3c --- /dev/null +++ b/test/cli/test/rejections-between-tests.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +// https://github.com/oven-sh/bun/issues/34859 — an unhandled rejection inside a +// test file's top-level async IIFE (outside any test()) must fail the file, +// matching how uncaught exceptions between tests are already reported. +describe("unhandled rejections between tests", () => { + test("file with only a rejecting async IIFE fails", () => { + using dir = tempDir("rejecting-iife", { + "rejecting.test.ts": ` +(async () => { + throw new Error("boom from async IIFE"); +})(); +`, + "ok.test.ts": ` +import { test, expect } from "bun:test"; +test("passes", () => { + expect(1).toBe(1); +}); +`, + }); + + const result = Bun.spawnSync([bunExe(), "test", "rejecting.test.ts"], { + cwd: dir, + env: bunEnv, + stdio: [null, "pipe", "pipe"], + }); + + const stderr = result.stderr.toString("utf-8"); + expect(stderr).toContain("boom from async IIFE"); + expect(stderr).toContain("Unhandled error"); + expect(result.exitCode).not.toBe(0); + }); + + test("rejection while a test is running still fails only that test", () => { + using dir = tempDir("rejecting-in-test", { + "in.test.ts": ` +import { test, expect } from "bun:test"; +test("passes", () => { + Promise.reject(new Error("async leak")); + expect(1).toBe(1); +}); +`, + }); + + const result = Bun.spawnSync([bunExe(), "test", "in.test.ts"], { + cwd: dir, + env: bunEnv, + stdio: [null, "pipe", "pipe"], + }); + + const stderr = result.stderr.toString("utf-8"); + expect(stderr).toContain("async leak"); + expect(result.exitCode).not.toBe(0); + }); +});