Skip to content
Open
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
24 changes: 5 additions & 19 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3923,29 +3923,15 @@ pub mod formatter {
writer_: &mut dyn bun_io::Write,
value: JSValue,
) -> JsResult<()> {
// Temporarily remove from the visited map to allow
// printErrorlikeObject to process it. The circular reference
// check is already done in print_as, so we know it's safe.
let was_in_map = if self.map_node.is_some() {
self.map.remove(&value).is_some()
} else {
false
};
let map_restore_ptr: *mut visited::Map = &raw mut self.map;
scopeguard::defer! {
// SAFETY: `self.map` outlives this guard; no other borrow is
// live at the drop point.
unsafe {
if was_in_map {
let _ = (*map_restore_ptr).insert(value, ());
}
}
}

// The value stays in the visited map so re-entrant property
// formatting hits the `[Circular]` guard.
Comment thread
robobun marked this conversation as resolved.
let mut adapter = DynWriteAdapter::new(&mut *writer_);
// SAFETY: per-thread VM.
let vm = VirtualMachine::get().as_mut();
vm.print_errorlike_object(value, None, None, self, adapter.interface(), C, false);
if self.global_this.has_exception() {
return Err(jsc::JsError::Thrown);
}
Ok(())
}

Expand Down
61 changes: 61 additions & 0 deletions test/regression/issue/circular-error-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,64 @@ test("error with circular reference in cause chain", async () => {
expect(stdout).not.toContain("Maximum call stack");
expect(stderr).not.toContain("Maximum call stack");
});

test.concurrent("uncaught error that is its own cause and its own errors entry", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `const e = new Error('cyc'); e.cause = e; e.errors = [e]; throw e;`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toContain("error: cyc");
expect(stderr).toContain("[Circular]");
expect(exitCode).toBe(1);
});

test.concurrent("console.log of error that is its own cause and its own errors entry", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const e = new Error('cyc'); e.cause = e; e.errors = [e]; console.log(e); console.log('after error print');`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toContain("[Circular]");
expect(stdout).toContain("after error print");
expect(exitCode).toBe(0);
});

test.concurrent("worker uncaught cyclic error reaches the parent error event intact", async () => {
using dir = tempDir("worker-cyclic-error", {
"index.js": `
const { Worker } = require("node:worker_threads");
const src = "const e = new Error('cyc'); e.cause = e; e.errors = [e]; throw e";
const w = new Worker(src, { eval: true });
w.on("error", e => console.log("error-event", e.name, JSON.stringify(e.message)));
w.on("exit", c => console.log("worker-exit", c));
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toContain('error-event Error "cyc"');
expect(stdout).toContain("worker-exit 1");
expect(stderr).not.toContain("ASSERTION FAILED");
expect(exitCode).toBe(0);
});