SIGN IN SIGN UP
denoland / deno UNCLAIMED

A modern runtime for JavaScript and TypeScript.

0 0 16 Rust

fix(test): don't kill the deno process on top-level Deno.exit() (#34564)

## Summary

Calling `Deno.exit()` from outside any test function — at module top
level, in an `unload` listener, or from async work that escaped a test —
used to kill the entire `deno test` process with the requested exit
code. That silently dropped buffered test results and made failing tests
"pass" with exit code 0.

Reproducer from #12888:

```ts
Deno.test("fail", () => { throw new Error("fail"); });
self.onunload = () => { Deno.exit(0); };
```

Before this PR:

```
$ deno test test.ts
Check file:///.../test.ts
running 1 test from file:///.../test.ts
test fail ... FAILED (9ms)
$ echo $?
0   # failed test, but zero exit code
```

After this PR:

```
$ deno test test.ts
running 1 test from ./test.ts
fail ... FAILED (1ms)

note ./test.ts called `Deno.exit(0)` from outside any test. The isolate was terminated; remaining test files will continue.

 ERRORS

fail => ./test.ts:1:6
error: Error: fail
...

FAILED | 0 passed | 1 failed (24ms)

error: Test failed
$ echo $?
1
```

## How it works

Each test specifier now installs a default exit handler (in addition to
the per-test `assertExit` handlers) that, when `Deno.exit(code)` is
called outside of any test function, calls a new `op_test_isolate_exit`
op:

- records the exit code in `OpState` (`IsolateExitInfo`)
- sends a new `TestEvent::IsolateExit(origin, exit_code)` to the
reporter
- asks V8 to terminate execution of just this isolate (uncatchable)

The test runner detects the termination via the `IsolateExitInfo` flag,
calls `cancel_terminate_execution`, and continues with any remaining
specifiers. A zero exit code is treated as a clean isolate teardown; a
non-zero exit code fails the run.

The existing `sanitizeExit: false` path (which kills the whole process
via `std::process::exit`) is preserved for `Deno.exit()` calls from
inside a test function — only the *outside-a-test* path changes here,
since that's where the silent-process-kill bug lived.

Fixes #12888.

Closes denoland/orchid#325

## Test plan

- [x] Existing `exit_sanitizer`, `exit_sanitizer_disabled`,
`exit_code{,2,3}`, `before_unload_prevent_default` spec tests still pass
- [x] New `exit_in_isolate_unload` spec test (the literal #12888
reproducer): test fails, isolate-exit note is printed, process exits 1
- [x] New `exit_in_isolate_top_level::exit_zero_alone`: top-level
`Deno.exit(0)` -> exit 0, no phantom tests
- [x] New `exit_in_isolate_top_level::exit_nonzero_alone`: top-level
`Deno.exit(7)` -> exit 1, failure reported
- [x] New
`exit_in_isolate_top_level::mixed_run_passing_alongside_isolate_exit`: a
passing test file run alongside an isolate-exit file still completes
normally
- [x] `cargo clippy --bin deno` clean

---------

Co-authored-by: divybot <divybot@users.noreply.github.com>
Co-authored-by: Divy Srivastava <me@littledivy.com>
E
em committed
5c6094ef7f8a0d213c2d2ee9afc8d263c025fad3
Parent: 902ba73
Committed by GitHub <noreply@github.com> on 5/31/2026, 4:29:27 AM