From 8195f1568d377f1fc667ac052ca9784d74ff4719 Mon Sep 17 00:00:00 2001 From: 0xfandom Date: Tue, 23 Jun 2026 14:30:11 +0530 Subject: [PATCH 1/4] fix(console): send console.trace to stderr with a "Trace:" prefix console.trace wrote the formatted arguments and stack trace to stdout with no prefix. Node writes them to stderr, prefixed with "Trace: " (or a bare "Trace" when called with no arguments). Route the Trace message type to the stderr writer and emit the prefix before the formatted arguments. Fixes #19952 --- src/jsc/ConsoleObject.rs | 15 +++++++++--- test/js/node/console/console.test.ts | 36 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 96455a9a6513..205104523fd4 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -454,7 +454,8 @@ fn message_with_type_and_level_( // Lock/unlock a mutex incase two JS threads are console.log'ing at the same // time. We do this the slightly annoying way to avoid assigning a pointer. let use_stderr = matches!(level, MessageLevel::Warning | MessageLevel::Error) - || message_type == MessageType::Assert; + || message_type == MessageType::Assert + || message_type == MessageType::Trace; let _stream_lock = ConsoleStreamLock::acquire(use_stderr); if message_type == MessageType::Clear { @@ -477,7 +478,9 @@ fn message_with_type_and_level_( return Ok(()); } - let enable_colors = if matches!(level, MessageLevel::Warning | MessageLevel::Error) { + let enable_colors = if matches!(level, MessageLevel::Warning | MessageLevel::Error) + || message_type == MessageType::Trace + { Output::enable_ansi_colors_stderr() } else { Output::enable_ansi_colors_stdout() @@ -496,7 +499,7 @@ fn message_with_type_and_level_( // long-lived `&mut ConsoleObject` across the re-derive in the empty-`Log` // arm below. let raw_writer: &mut bun_core::io::Writer = unsafe { - if matches!(level, MessageLevel::Warning | MessageLevel::Error) { + if matches!(level, MessageLevel::Warning | MessageLevel::Error) || message_type == MessageType::Trace { (*console).error_writer() } else { (*console).writer() @@ -578,6 +581,12 @@ fn message_with_type_and_level_( } } + // console.trace prints "Trace: " (or just "Trace" with no args), + // matching Node, with the stack trace appended below. + if message_type == MessageType::Trace { + let _ = writer.write_all(if print_length > 0 { b"Trace: " } else { b"Trace\n" }); + } + if print_length > 0 { format2( level, diff --git a/test/js/node/console/console.test.ts b/test/js/node/console/console.test.ts index 2913817f23c8..2dc9a78e02ba 100644 --- a/test/js/node/console/console.test.ts +++ b/test/js/node/console/console.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { Console } from "node:console"; +import { bunEnv, bunExe } from "harness"; import { Writable } from "node:stream"; @@ -88,3 +89,38 @@ test("console._stderr", () => { configurable: true, }); }); + +// console.trace writes to stderr with a "Trace:" prefix, matching Node. +// https://github.com/oven-sh/bun/issues/19952 +describe("console.trace", () => { + async function run(code: string) { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", code], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + } + + test("goes to stderr, not stdout", async () => { + const { stdout, stderr, exitCode } = await run(`console.trace("hi")`); + expect(stdout).toBe(""); + expect(stderr).toStartWith("Trace: hi\n"); + expect(stderr).toContain("at "); + expect(exitCode).toBe(0); + }); + + test("no arguments prints bare 'Trace'", async () => { + const { stdout, stderr } = await run(`console.trace()`); + expect(stdout).toBe(""); + expect(stderr).toStartWith("Trace\n"); + expect(stderr).toContain("at "); + }); + + test("applies format specifiers", async () => { + const { stderr } = await run(`console.trace("x=%d", 5)`); + expect(stderr).toStartWith("Trace: x=5\n"); + }); +}); From 9e634ca590bfee8fa955feb2db71aa24a1a70929 Mon Sep 17 00:00:00 2001 From: 0xfandom Date: Thu, 25 Jun 2026 13:40:06 +0530 Subject: [PATCH 2/4] test(console): assert exit code in console.trace subprocess cases --- test/js/node/console/console.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/js/node/console/console.test.ts b/test/js/node/console/console.test.ts index 2dc9a78e02ba..3b6f971fdec1 100644 --- a/test/js/node/console/console.test.ts +++ b/test/js/node/console/console.test.ts @@ -113,14 +113,17 @@ describe("console.trace", () => { }); test("no arguments prints bare 'Trace'", async () => { - const { stdout, stderr } = await run(`console.trace()`); + const { stdout, stderr, exitCode } = await run(`console.trace()`); expect(stdout).toBe(""); expect(stderr).toStartWith("Trace\n"); expect(stderr).toContain("at "); + expect(exitCode).toBe(0); }); test("applies format specifiers", async () => { - const { stderr } = await run(`console.trace("x=%d", 5)`); + const { stdout, stderr, exitCode } = await run(`console.trace("x=%d", 5)`); + expect(stdout).toBe(""); expect(stderr).toStartWith("Trace: x=5\n"); + expect(exitCode).toBe(0); }); }); From d699e1bcadc584fdf8a19e965ea23a1fda81d129 Mon Sep 17 00:00:00 2001 From: 0xfandom Date: Thu, 23 Jul 2026 12:31:46 +0530 Subject: [PATCH 3/4] fix(console): emit the trace label after the console.group indent format2 writes the indent as its first bytes, so writing "Trace: " before calling it put the label ahead of the group indent, and the no-args path skipped the indent entirely. Thread the label through FormatOptions as a prefix that format2 emits right after the indent, and indent the bare header, matching Node. --- src/jsc/ConsoleObject.rs | 17 +++++++++++++++-- test/js/node/console/console.test.ts | 12 ++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 205104523fd4..1d8ee3205247 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -582,9 +582,16 @@ fn message_with_type_and_level_( } // console.trace prints "Trace: " (or just "Trace" with no args), - // matching Node, with the stack trace appended below. + // matching Node, with the stack trace appended below. The label goes through + // FormatOptions so it lands after the console.group indent rather than + // before it. if message_type == MessageType::Trace { - let _ = writer.write_all(if print_length > 0 { b"Trace: " } else { b"Trace\n" }); + if print_length > 0 { + print_options.prefix = b"Trace: "; + } else { + let _ = formatter::write_indent_n(u32::from(default_indent), writer); + let _ = writer.write_all(b"Trace\n"); + } } if print_length > 0 { @@ -1307,6 +1314,9 @@ pub struct FormatOptions { pub single_line: bool, pub default_indent: u16, pub error_display_level: ErrorDisplayLevel, + /// Label emitted immediately after the indent, before the formatted values + /// (used by `console.trace` for its "Trace: " prefix). + pub prefix: &'static [u8], } impl Default for FormatOptions { @@ -1321,6 +1331,7 @@ impl Default for FormatOptions { single_line: false, default_indent: 0, error_display_level: ErrorDisplayLevel::Full, + prefix: b"", } } } @@ -1493,6 +1504,7 @@ pub fn format2( if fmt.write_indent(writer).is_err() { return Ok(()); } + let _ = writer.write_all(options.prefix); if matches!(tag.tag, TagPayload::String) { if options.enable_colors { @@ -1557,6 +1569,7 @@ pub fn format2( if fmt.write_indent(writer).is_err() { return Ok(()); } + let _ = writer.write_all(options.prefix); let mut any = false; if options.enable_colors { diff --git a/test/js/node/console/console.test.ts b/test/js/node/console/console.test.ts index 3b6f971fdec1..5b17b383f957 100644 --- a/test/js/node/console/console.test.ts +++ b/test/js/node/console/console.test.ts @@ -126,4 +126,16 @@ describe("console.trace", () => { expect(stderr).toStartWith("Trace: x=5\n"); expect(exitCode).toBe(0); }); + + test("label goes after the console.group indent", async () => { + const { stdout, stderr, exitCode } = await run( + `console.group("G"); console.trace("x"); console.trace(); console.groupEnd(); console.trace("top");`, + ); + // The group indent precedes the label, and the bare header is indented too. + expect(stderr).toStartWith(" Trace: x\n"); + expect(stderr).toContain("\n Trace\n"); + expect(stderr).toContain("\nTrace: top\n"); + expect(stdout).toBe("G\n"); + expect(exitCode).toBe(0); + }); }); From c02d6dac1c743510555f7662f76bd898033ba603 Mon Sep 17 00:00:00 2001 From: 0xfandom Date: Fri, 31 Jul 2026 13:30:40 +0530 Subject: [PATCH 4/4] test(console): assert the grouped trace headers in order --- test/js/node/console/console.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/js/node/console/console.test.ts b/test/js/node/console/console.test.ts index 5b17b383f957..f69f7e58b90b 100644 --- a/test/js/node/console/console.test.ts +++ b/test/js/node/console/console.test.ts @@ -132,9 +132,8 @@ describe("console.trace", () => { `console.group("G"); console.trace("x"); console.trace(); console.groupEnd(); console.trace("top");`, ); // The group indent precedes the label, and the bare header is indented too. - expect(stderr).toStartWith(" Trace: x\n"); - expect(stderr).toContain("\n Trace\n"); - expect(stderr).toContain("\nTrace: top\n"); + const headers = stderr.match(/^(?: Trace: x| Trace|Trace: top)$/gm); + expect(headers).toEqual([" Trace: x", " Trace", "Trace: top"]); expect(stdout).toBe("G\n"); expect(exitCode).toBe(0); });