Skip to content

console: route console.trace() output to stderr - #36506

Closed
robobun wants to merge 1 commit into
mainfrom
farm/14d5d27c/console-trace-stderr
Closed

console: route console.trace() output to stderr#36506
robobun wants to merge 1 commit into
mainfrom
farm/14d5d27c/console-trace-stderr

Conversation

@robobun

@robobun robobun commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Problem

console.trace() writes to stdout. Node.js writes it to stderr.

$ bun -e 'console.trace("hello")' >/dev/null
$ # no output

$ node -e 'console.trace("hello")' >/dev/null
Trace: hello
    at [eval]:1:9
    ...

Cause

message_with_type_and_level in src/jsc/ConsoleObject.rs selects the output stream based on MessageLevel::Warning | MessageLevel::Error. JSC dispatches console.trace() with MessageType::Trace and MessageLevel::Log, so it fell through to stdout. The stream lock, color selection, and writer selection each repeated the same incomplete condition.

Fix

Add MessageType::Trace to the use_stderr computation and reuse that flag for the color and writer selection so all three stay consistent. This is the fix from #35058 by @hanu-14 with the three duplicated conditions folded into the existing use_stderr variable.

This PR is scoped to the stderr routing only. #32638 and #20020 also add a Node-style Trace: header before the message; that is a separate formatting change not included here (#36471 covers it via the JS wrapper path).

Fixes #19952
Closes #35058
Closes #35040

Verification

New test/js/web/console/console-trace.test.ts asserts that console.trace() (with and without arguments) writes nothing to stdout and writes both the message and the stack to stderr. All three cases fail on main and pass with this change.

bun bd test test/js/web/console/ test/js/bun/console/ test/js/node/console/
# 107 pass, 1 skip, 0 fail

[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/console/console-trace.test.ts
bun test v1.4.0 (abc5a163f)

test/js/web/console/console-trace.test.ts:
22 |     env: bunEnv,
23 |     stdout: "pipe",
24 |     stderr: "pipe",
25 |   });
26 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
27 |   expect(stdout).toBe("");
                      ^
error: expect(received).toBe(expected)

- ""
+ "      at /workspace/bun/[eval]:1:9
+ "

- Expected  - 1
+ Received  + 2

      at <anonymous> (/workspace/bun/test/js/web/console/console-trace.test.ts:27:18)
(fail) console.trace() with no arguments writes the stack to stderr [413.95ms]
 8 |     env: bunEnv,
 9 |     stdout: "pipe",
10 |     stderr: "pipe",
11 |   });
12 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
13 |   expect(stdout).toBe("");
                      ^
error: expect(received).toBe(expected)

- ""
+ "marker
+       at /workspace/bun/[eval]:1:9
+ "

- Expected  - 1
+ Received  + 3

      at <a
... (truncated)

release without fix: 3 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/js/web/console/console-trace.test.ts:
 8 |     env: bunEnv,
 9 |     stdout: "pipe",
10 |     stderr: "pipe",
11 |   });
12 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
13 |   expect(stdout).toBe("");
                      ^
error: expect(received).toBe(expected)

- ""
+ "marker
+       at /workspace/bun/[eval]:1:9
+ "

- Expected  - 1
+ Received  + 3

      at <anonymous> (/workspace/bun/test/js/web/console/console-trace.test.ts:13:18)
(fail) console.trace() writes to stderr, not stdout [27.80ms]
35 |     env: bunEnv,
36 |     stdout: "pipe",
37 |     stderr: "pipe",
38 |   });
39 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
40 |   expect(stdout).toBe("before\nafter\n");
                      ^
error: expect(received).toBe(expected)

  "before
+ traced
+       at /workspace/bun/[eval]:1:32
  after
  "

- Expected  - 0
+ Received  + 2

      at <anonymous> (/workspace/bun/test/js/web/console/console-trace.test.ts:40:18)
(fail) console.trace() does not interleave with console.log() on stdout
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/console/console-trace.test.ts
bun test v1.4.0 (abc5a163f)

test/js/web/console/console-trace.test.ts:
(pass) console.trace() does not interleave with console.log() on stdout [420.90ms]
(pass) console.trace() with no arguments writes the stack to stderr [932.18ms]
(pass) console.trace() writes to stderr, not stdout [985.33ms]

 3 pass
 0 fail
 10 expect() calls
Ran 3 tests across 1 file. [4.02s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     abc5a163f2
  features     baseline

22 deps, 108 codegen, 1171 objects in 2519ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1234] fetch tinycc
[tinycc] up to date
[2/1234] gen ErrorCode+*.h
[3/1234] gen bindgenv2
[4/1234] gen .bind.ts → GeneratedBindings.cpp
[5/1234] fetch zlib
[zlib] up to date
[6/1234] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[7/1234] fetch picohttpparser
[picohttpparser] up to date
[8/1234] install /workspace/bun
bun install v1.4.0-canary.1 (1498d7b77)

Checked 124 installs across 170 packages (no changes) [119.00ms]
[9/1234] subst deps/zlib/zconf.h
[10/1234] fetch brotli
[brotli] up to date
[11/1234] subst deps/zlib/zlib.h
[12/1234] host-cc deps/tinycc/codegen-tool
[13/1234] fetch nodejs (prebuilt)
[nodejs] up to date
[14/1234] fetch libdeflate
[libdeflate] up to date
[15/1234] fetch zstd
[zstd] up to date
[16/1234] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (
... (truncated)
diff hotspot
src/jsc/ConsoleObject.rs                  |  7 ++---
 test/js/web/console/console-trace.test.ts | 43 +++++++++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 3 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                       reads  edits  tests
src/jsc/ConsoleObject.rs                       2      3      0
test/js/web/console/console-trace.test.ts      0      1      0

self-review · 1 surviving concern

24 concerns were raised and did not survive verification.

root cause · written by the author bot

The stderr routing logic in ConsoleObject.rs decided the output stream solely from the message level, checking only for Warning or Error, so console.trace() was written to stdout instead of stderr as Node.js does. The fix extends the use_stderr computation to also account for MessageType::Trace and reuses that flag at the color and writer selection sites rather than repeating the level check. This routes trace output to stderr without altering behavior for any other console method.

console.trace() was writing to stdout because the stream selection in
message_with_type_and_level only checked MessageLevel::Warning/Error and
JSC dispatches Trace with MessageLevel::Log. Node.js writes console.trace()
to stderr.

Add MessageType::Trace to the use_stderr condition and reuse that flag for
the color and writer selection so the three stay in sync.

Fixes #19952

Co-authored-by: MOHAMMED HANAN M T P <91409429+hanu-14@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 230f1e3b-3944-4e2b-b864-e605313648f3

📥 Commits

Reviewing files that changed from the base of the PR and between 468dac3 and abc5a16.

📒 Files selected for processing (2)
  • src/jsc/ConsoleObject.rs
  • test/js/web/console/console-trace.test.ts

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix: console.trace should go to stderr #20020 - Also fixes console.trace() going to stdout instead of stderr
  2. fix(console): send console.trace to stderr with a "Trace:" prefix #32638 - Also fixes console.trace() to stderr, adds a 'Trace:' prefix
  3. Fix console.trace() going to stdout instead of stderr #35040 - Also fixes console.trace() going to stdout instead of stderr
  4. fix: route console.trace() to stderr #35058 - Also fixes console.trace() routing to stderr
  5. console: route count/table/group/time*/trace/assert through this.log/warn/error #36471 - Broader console refactor that also routes console.trace to stderr

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — routes console.trace() to stderr to match Node.

What was reviewed:

  • use_stderr now includes MessageType::Trace; the two duplicated matches!(level, Warning | Error) sites are folded into it — checked that the added Assert coverage in the color/writer selection is a no-op (JSC dispatches assert with MessageLevel::Error, and the len==0 arm already used error_writer()).
  • Confirmed use_stderr is computed after the StartGroup/EndGroup early returns and before Clear, so the lock still guards the right stream on every remaining path.
  • Tests drain both pipes concurrently, use bunEnv/bunExe/await using, assert stdout is empty and stderr carries both the message and a stack frame, and would fail on main.
Extended reasoning...

Overview

Two-file change: src/jsc/ConsoleObject.rs adds MessageType::Trace to the use_stderr computation in message_with_type_and_level_, and reuses that flag for the ANSI-color and writer selection instead of repeating matches!(level, Warning | Error) at each site. A new test/js/web/console/console-trace.test.ts adds three concurrent subprocess tests covering console.trace(msg), console.trace() with no args, and interleaving with console.log.

Security risks

None. This only changes which fd (stdout vs stderr) a diagnostic message is written to; no untrusted-input parsing, no allocation, no FFI surface change.

Level of scrutiny

Low. This is a narrow Node-compat fix that has been proposed multiple times (#35058, #35040, #32638, #20020) — the PR consolidates the fix and folds the three duplicated conditions into one flag as REVIEW.md's dedup guidance calls for. I checked the one non-obvious consequence of the dedup: Assert is now included in the color/writer selection via use_stderr, whereas before only the level check applied. JSC's ConsoleClient dispatches console.assert with MessageLevel::Error, so the level check already routed it to stderr; the len==0 assert arm at line 420 also hard-codes error_writer()/enable_ansi_colors_stderr(). So no behavior change beyond Trace.

Other factors

Tests follow harness conventions exactly: it.concurrent for independent spawns, await using on the proc, Promise.all over stdout/stderr/exited to avoid pipe deadlock, exit-code asserted last, and the issue URL as the only comment. The assertions are specific enough to fail on main (stdout must be exactly "" / "before\nafter\n", stderr must contain the marker and "at "). No existing test asserts trace output on stdout, so no snapshots need updating.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate: #37128 reworks console output as a whole and routes console.trace() to stderr (checked on its current head: message and stack land on stderr, nothing on stdout), so the change here is covered there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console.trace() goes to stdout instead of stderr

1 participant