Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,16 @@ impl VirtualMachine {
> 0)
}

/// An error nothing in script handled was reported and ends this run.
/// `--hot`/`--watch` report such errors too but run on until the next reload.
Comment thread
robobun marked this conversation as resolved.
pub fn has_fatal_unhandled_error(&self) -> bool {
self.unhandled_error_counter > 0 && !self.is_watcher_enabled()
}

pub fn is_event_loop_alive(&self) -> bool {
if self.has_fatal_unhandled_error() {
return false;
}
let el = self.event_loop_shared();
self.is_event_loop_alive_excluding_immediates()
|| !el.immediate_tasks.is_empty()
Expand Down
101 changes: 100 additions & 1 deletion test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawnSync, which } from "bun";
import { describe, expect, it } from "bun:test";
import { familySync } from "detect-libc";
import { bunEnv, bunExe, isMacOS, isWindows, tempDir, tmpdirSync } from "harness";
import { bunEnv, bunExe, forEachLine, isMacOS, isWindows, tempDir, tmpdirSync } from "harness";
import { basename, join, resolve } from "path";

const process_sleep = resolve(import.meta.dir, "process-sleep.js");
Expand Down Expand Up @@ -972,6 +972,105 @@ describe.concurrent(() => {
});
});

describe("fatal error while immediates are pending", () => {
// Node runs nothing after an uncaught exception or unhandled rejection that
// nothing handled: the process exits from the error itself. This immediate
// requeues itself forever; if the run loop keeps turning after the error it
// says so and exits 42 rather than spinning until the test times out.
const requeueingImmediate = `
let errored = false;
let runsAfterError = 0;
setImmediate(function again() {
if (errored && ++runsAfterError === 50) {
console.log("immediates still running after the error");
process.exit(42);
}
setImmediate(again);
});`;

it("an uncaught exception exits 1 instead of being kept alive by a requeueing setImmediate", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`${requeueingImmediate}
process.on("exit", c => console.log("exit", c));
setTimeout(() => { errored = true; throw new Error("boom"); }, 1);`,
],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]);
expect(stdout).toBe("exit 1\n");
expect(stderr).toInclude("error: boom");
expect(exitCode).toBe(1);
});

it("an unhandled rejection exits 1 instead of being kept alive by a requeueing setImmediate", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`${requeueingImmediate}
setTimeout(() => { errored = true; Promise.reject(new Error("boom")); }, 1);`,
],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toInclude("error: boom");
expect(exitCode).toBe(1);
});

it("an unhandled rejection from a beforeExit listener exits 1 instead of being kept alive by a requeueing setImmediate", async () => {
// Same thing for the drain that follows 'beforeExit', which is a second
// run loop of its own.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`process.once("beforeExit", () => {
${requeueingImmediate}
errored = true;
Promise.reject(new Error("boom"));
});`,
],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toInclude("error: boom");
expect(exitCode).toBe(1);
});

it.each(["--hot", "--watch"])("%s still runs the immediates after the error", async flag => {
// The watcher modes print the error and keep the process alive for the
// next reload instead of exiting on it, so what was running keeps running.
using dir = tempDir("process-fatal-immediates", {
"index.js": `${requeueingImmediate}
setTimeout(() => { errored = true; throw new Error("boom"); }, 1);`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), flag, "index.js"],
cwd: String(dir),
env: bunEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr = proc.stderr.text();
const stdoutLines = forEachLine(proc.stdout);
const { value: firstLine } = await stdoutLines.next();
// What a watcher makes of the fixture's process.exit() is not under test
// here: stop the fixture ourselves once it has reported, then drain it.
proc.kill();
Comment thread
robobun marked this conversation as resolved.
await Array.fromAsync(stdoutLines);
expect(await stderr).toInclude("error: boom");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(firstLine).toBe("immediates still running after the error");
await proc.exited;
});
});

describe("process.onExit", () => {
it("throwing inside preserves exit code", async () => {
await using proc = Bun.spawn({
Expand Down
Loading