Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,13 @@ impl VirtualMachine {
}

pub fn is_event_loop_alive(&self) -> bool {
// An error nothing in script handled ends the run (node exits from the
// error itself): pending immediates must not keep it turning any more
// than pending timers or I/O do. `--hot`/`--watch` count such errors too
// but keep running until the next reload; for them the terms below decide.
Comment thread
robobun marked this conversation as resolved.
Outdated
if self.unhandled_error_counter > 0 && !self.is_watcher_enabled() {
return false;
}
let el = self.event_loop_shared();
self.is_event_loop_alive_excluding_immediates()
|| !el.immediate_tasks.is_empty()
Expand Down
97 changes: 96 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,101 @@ 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 { value: firstLine } = await forEachLine(proc.stdout).next();
proc.kill();
Comment thread
robobun marked this conversation as resolved.
await proc.exited;
expect(firstLine).toBe("immediates still running after the error");
expect(await stderr).toInclude("error: boom");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});

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