Skip to content

process: print Node-style warning report and hide internal frames in emitWarning - #34608

Closed
robobun wants to merge 8 commits into
mainfrom
claude/farm/2e9dda83/fix-deprecation-warning-output
Closed

robobun wants to merge 8 commits into
mainfrom
claude/farm/2e9dda83/fix-deprecation-warning-output

Conversation

@robobun

@robobun robobun commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes three presentation problems in the process.emitWarning / util.deprecate warning path. Semantics (once-law, [code] dedup, this/args/return passthrough, 'warning' event payload, --no-deprecation, --throw-deprecation exit 1) were already Node-correct; only the stderr report and thrown-error stack were off.

Fixes #9867. Supersedes #32433.

Repro

import { spawnSync } from "node:child_process";
const run = (args, src) => spawnSync(process.execPath, [...args, "-e", src], { encoding: "utf8" });
const LISTENER = `const util=require("node:util");process.on("warning",()=>{});util.deprecate(()=>1,"dep msg A","DEP0T1")();setTimeout(()=>{},30);`;
const PLAIN = `const util=require("node:util");util.deprecate(()=>1,"dep msg B","DEP0T2")();`;

console.log(run([], LISTENER).stderr.split("\n")[0]);
// node:   "(node:PID) [DEP0T1] DeprecationWarning: dep msg A"
// before: ""  (any 'warning' listener suppressed the stderr report entirely)

console.log(run([], PLAIN).stderr.split("\n")[0]);
// node:   "(node:PID) [DEP0T2] DeprecationWarning: dep msg B"
// before: "DeprecationWarning: dep msg B" then a separate ` code: "DEP0T2"` line

const r = run(["--throw-deprecation"], PLAIN);
console.log(/getDeprecationWarningEmitter|@undefined/.test(r.stderr));
// node:   false (warning + user stack)
// before: true (shows builtin source with raw `code !== @undefined` JSC intrinsic syntax)

Cause

jsFunction_emitWarning (the nextTick handler) emitted the 'warning' event or printed via the console error inspector, never both:

if (process->wrapped().hasEventListeners(ident)) {
    process->wrapped().emit(ident, args);
} else if (!Bun__NODE_NO_WARNINGS()) {
    Bun__ConsoleObject__messageWithTypeAndLevel(...);  // inspected error, no [CODE] form
}

so installing any listener silenced the on-terminal deprecation trail, and when it did print it used the error inspector instead of Node's one-line form. Separately, Process::emitWarning built the warning Error with createError() but never re-captured its stack past the supplied ctor, leaving the internal util/deprecate emitter frames on top; under --throw-deprecation the uncaught-error reporter then showed a source snippet from the bundled builtin (complete with @undefined intrinsic syntax).

Fix

  • Always emit 'warning', then (unless NODE_NO_WARNINGS) call a new internal/process/warning onWarning that writes Node's (name:pid) [CODE] Name: msg line, detail, and the one-time --trace-* hint via process.stderr.write. A throwing warning.toString() falls back to Error.prototype.toString so the printer itself never escalates a diagnostic into an uncaught exception.
  • Factor the body of Error.captureStackTrace into captureStackTraceForError and call it from Process::emitWarning with ctor so the warning's stack starts at the user's call site.

Verification

bun bd test test/js/node/process/process-warning-output.test.ts

Node parallel tests test-process-warning.js, test-process-emitwarning.js, test-process-no-deprecation.js, test-child-process-no-deprecation.js, test-util-deprecate.js, test-common-expect-warning.js, and test/js/node/v8/capture-stack-trace.test.js all pass.


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

fails on main (without fix)
ASAN without fix: 8 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/process/process-warning-output.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (5f701eb79)

test/js/node/process/process-warning-output.test.ts:
42 | 
43 |   test.concurrent("process.emitWarning with code and detail formats Node-style", async () => {
44 |     const src = `process.emitWarning("plain msg", { type: "CustomWarning", code: "CODE9", detail: "some detail" });`;
45 |     const { stderr, exitCode } = await run([], src);
46 |     const lines = stderr.split("\n");
47 |     expect(lines[0]).toMatch(/^\(\w+:\d+\) \[CODE9\] CustomWarning: plain msg$/);
                          ^
error: expect(received).toMatch(expected)

Expected substring or pattern: /^\(\w+:\d+\) \[CODE9\] CustomWarning: plain msg$/
Received: "CustomWarning: plain msg"

      at <anonymous> (/workspace/bun/test/js/node/p
... (truncated)

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (e6a0c1d0c)

test/js/node/process/process-warning-output.test.ts:
(pass) process.emitWarning default stderr report > NODE_NO_WARNINGS=1 still delivers the event but suppresses stderr [17.02ms]
(pass) process.emitWarning default stderr report > --throw-deprecation does not leak internal builtin source [20.00ms]
(pass) process.emitWarning default stderr report > process.emitWarning with code and detail formats Node-style [25.99ms]
106 |         console.log(JSON.stringify({ stack: e.stack === orig, code: e.code, detail: e.detail }));
107 |       }
108 |       outer();
109 |     `;
110 |     const { stdout, exitCode } = await run([], src);
111 |     expect(JSON.parse(stdout)).toEqual({ stack: true, code: undefined, detail: undefined });
                                     ^
error: expect(received).toEqual(expected)

  {
-   "code": undefined,
-   "detail": undefined,
+   "code": "CODE",
+   "detail": "Y",
    "stack": true,
  }

- Expected  - 2
+ Received  + 2

      at <anonymous> (/workspace/bun/test/js/node/process/process-warning-output.test.ts:111:32)
(fail) process.emitWarning default stderr report > passing an Error instance leaves it un
... (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/node/process/process-warning-output.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (5f701eb79)

test/js/node/process/process-warning-output.test.ts:
(pass) process.emitWarning default stderr report > NODE_NO_WARNINGS=1 still delivers the event but suppresses stderr [578.67ms]
(pass) process.emitWarning default stderr report > --throw-deprecation does not leak internal builtin source [909.74ms]
(pass) process.emitWarning default stderr report > process.emitWarning with code and detail formats Node-style [1296.79ms]
(pass) process.emitWarning default stderr report > default report uses Node's one-line (name:pid) [CODE] form [1605.10ms]
(pass) process.emitWarning default stderr report > a user 'warning' listener does not suppress the stderr report [1745.29ms]
(pass) process.emitWarning default stderr
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 890ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/22] gen cpp.rs (cppbind)
[2/22] gen BunProcess.lut.h
Generating /workspace/bun/build/release/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[3/22] gen JS modules (bundle-modules)
Preprocess modules (8098ms)
Bundle modules (49ms)
Postprocesss modules (236ms)
Bundle Functions (782ms)
Generate Code (110ms)

[9.29s] Bundled "src/js" for production
  2038 kb
  166 internal modules
  13 native modules
  90 internal functions across 19 files
[3/10] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component
... (truncated)
diff hotspot
src/js/internal/process/warning.ts                 |  76 ++++++++++++
 src/jsc/bindings/BunProcess.cpp                    |  53 ++++----
 src/jsc/bindings/FormatStackTraceForJS.cpp         |  46 ++++---
 src/jsc/bindings/FormatStackTraceForJS.h           |   4 +
 .../js/node/process/process-warning-output.test.ts | 136 +++++++++++++++++++++
 5 files changed, 266 insertions(+), 49 deletions(-)

gate history · 4 passed · 0 rejected · iteration 2

evidence per changed file
file                                                 reads  edits  tests
src/js/internal/process/warning.ts                       3      7      0
src/jsc/bindings/BunProcess.cpp                          6      7      0
src/jsc/bindings/FormatStackTraceForJS.cpp               3      3      0
src/jsc/bindings/FormatStackTraceForJS.h                 1      1      0
test/js/node/process/process-warning-output.test.ts      3      7      0

…tWarning

The nextTick warning handler only printed to stderr when no 'warning'
listener was installed, so adding any listener silenced the on-terminal
deprecation trail. When it did print it used the error inspector instead
of Node's one-line "(node:PID) [CODE] Name: msg" form, and the Error
constructed for a string warning kept the internal util/deprecate frames
on its stack, so --throw-deprecation surfaced a snippet of builtin source
(with raw JSC intrinsic syntax) in the uncaught-error report.

Always emit the 'warning' event and, unless NODE_NO_WARNINGS, call a new
internal onWarning printer that writes Node's format via process.stderr.
When a ctor is passed to process.emitWarning, re-capture the warning's
stack with it as the caller so only user frames remain. The
Error.captureStackTrace body is factored into captureStackTraceForError
for reuse.
@coderabbitai

coderabbitai Bot commented Jul 18, 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: 11 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: 1ac136d8-0a90-4d22-9d1e-d7bd785688f7

📥 Commits

Reviewing files that changed from the base of the PR and between 511caaa and 5f701eb.

📒 Files selected for processing (5)
  • src/js/internal/process/warning.ts
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/FormatStackTraceForJS.cpp
  • src/jsc/bindings/FormatStackTraceForJS.h
  • test/js/node/process/process-warning-output.test.ts

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

@robobun

robobun commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:22 AM PT - Jul 18th, 2026

❌ @robobun, your commit 5f701eb has 1 failures in Build #75422 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34608

That installs a local version of the PR into your bun-34608 executable, so you can run:

bun-34608 --bun

Comment thread src/js/internal/process/warning.ts Resolved
robobun added 2 commits July 18, 2026 09:54
…tack

The hint line now points at a flag that actually does something: when
either --trace-* flag is present in execArgv, onWarning prints the
warning's stack (already trimmed to the user call site when a ctor was
supplied) and skips the hint. Also replace the exact-empty stderr
assertion in the NODE_NO_WARNINGS test with a negative match on the
report text.
Comment thread src/jsc/bindings/BunProcess.cpp Resolved
Node's ErrorCaptureStackTrace(warning, ctor) runs inside createWarningObject,
which is only reached when the warning argument is a string. A caller-supplied
Error is emitted as-is. Moving the captureStackTraceForError call inside the
string branch restores that, so process.emitWarning(err, {ctor}) / the 4-arg
form no longer overwrite err.stack.

@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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/jsc/bindings/BunProcess.cpp:2018-2021 — Once this moves inside the warning.isString() branch (per the earlier comment on this block), also fall back to process.emitWarning as the filter when ctor is not callable — Node calls ErrorCaptureStackTrace(warning, ctor || process.emitWarning) unconditionally for string warnings. As-is, the common no-ctor form process.emitWarning('msg', 'Type', 'CODE') skips the re-capture entirely and leaves an at emitWarning (native) frame at the top of warning.stack under --trace-warnings. The removed TODO comment quoted exactly this fallback.

    Extended reasoning...

    What the bug is

    Node's createWarningObject in lib/internal/process/warning.js calls ErrorCaptureStackTrace(warning, ctor || process.emitWarning) unconditionally for every string warning — when the caller passes no ctor, Node falls back to process.emitWarning as the filter, so the emitWarning frame itself is always stripped. The removed TODO comment in this diff literally quoted that line: // TODO: ErrorCaptureStackTrace(warning, ctor || process.emitWarning);.

    The new code at BunProcess.cpp:2018 gates the whole re-capture on if (ctor.isCallable()) with no fallback:

    if (ctor.isCallable()) {
        Bun::captureStackTraceForError(globalObject, errorInstance, ctor);
        RETURN_IF_EXCEPTION(scope, {});
    }

    So the common forms process.emitWarning("msg") and process.emitWarning("msg", "Type", "CODE") — where ctor arrives as jsUndefined() — skip the re-capture entirely and keep the stack that createError() collected inside Process_emitWarning. Process_emitWarning is registered in the static hash table at line ~4463 as a plain public host function (default ImplementationVisibility::Public), so JSC's ErrorInstance::finishCreation → Interpreter::getStackTrace captures its frame at the top.

    Step-by-step proof

    1. User runs bun --trace-warnings -e 'process.emitWarning("trace me", "CustomWarning", "CODE10")'.
    2. Process_emitWarning is invoked with warning="trace me", type="CustomWarning", code="CODE10", ctor=undefined.
    3. Line 2003: warning.isString() → errorInstance = createError(globalObject, "trace me"). createError captures a stack from vm.topCallFrame — the top frame is the Process_emitWarning host call.
    4. Line 2018: ctor.isCallable() is false (undefined) → no captureStackTraceForError call. The createError stack is kept as-is.
    5. onWarning runs with trace = true and prints warning.stack. Output contains at emitWarning (native) above the user's [eval] frame.

    Node's output for the same command shows only the user's call site. The new test at process-warning-output.test.ts:99-106 only asserts /^\s+at /m and does not check for absence of the emitWarning frame, so it passes either way.

    Why existing code doesn't prevent it

    Before this PR the entire block was a commented-out TODO, so the no-ctor path never re-captured — this isn't a regression, but the PR's title is "hide internal frames in emitWarning" and the no-ctor form is by far the most common call shape. Per REVIEW.md's "Fix the whole class in the same PR", the sibling no-ctor path should get the same treatment.

    Relationship to the earlier comment on this block

    This is not a duplicate of the earlier comment on lines 2018-2021 — that one is about over-application (running the re-capture on user-supplied Error instances). This is about under-application (skipping the re-capture for string warnings when ctor is omitted). Both fixes land at the same location and can be done together.

    How to fix

    Once moved inside the if (warning.isString()) branch per the earlier comment, add the || process.emitWarning fallback:

    if (warning.isString()) {
        auto s = warning.getString(globalObject);
        errorInstance = createError(globalObject, !s.isEmpty() ? s : "Warning"_s);
        errorInstance->putDirect(vm, vm.propertyNames->name, type, JSC::PropertyAttribute::DontEnum | 0);
        JSValue filter = ctor.isCallable()
            ? ctor
            : process->get(globalObject, Identifier::fromString(vm, "emitWarning"_s));
        RETURN_IF_EXCEPTION(scope, {});
        Bun::captureStackTraceForError(globalObject, errorInstance, filter);
        RETURN_IF_EXCEPTION(scope, {});
    }

    Optionally tighten the --trace-warnings test to assert expect(stderr).not.toMatch(/at emitWarning/).

    Impact

    One extra at emitWarning (native) line at the top of the printed stack under --trace-warnings / --trace-deprecation, and in warning.stack as read by any 'warning' listener. Purely cosmetic Node-compat gap — nothing breaks — hence nit, but it's a one-liner squarely in this PR's stated scope.

Comment thread src/jsc/bindings/BunProcess.cpp Resolved
Comment thread src/js/internal/process/warning.ts Resolved
Comment thread src/js/internal/process/warning.ts Resolved
- Re-capture the synthesized warning's stack unconditionally (not gated on
  ctor), so the enclosing host-function frame is dropped even for
  process.emitWarning('msg') with no ctor and --trace-warnings starts at
  the user's call site.
- Honor runtime process.traceProcessWarnings / process.traceDeprecation in
  addition to the execArgv flags.
- Strip a trailing .exe from the hint-line argv0 on Windows.
@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Re: the no-ctor fallback note in the additional-findings comment: done in fe65d4c. The synthesized warning now re-captures unconditionally; when ctor is not callable we pass jsUndefined() (not process.emitWarning) because getFramesForCaller's single framesToSkip already drops the enclosing host-function frame, and passing process.emitWarning there would then not find it and clear the whole stack. --trace-warnings with process.emitWarning('msg', 'Type', 'CODE') now starts at the user's call site with no at emitWarning frame; the test asserts that.

Comment thread src/jsc/bindings/BunProcess.cpp Resolved
Comment thread src/jsc/bindings/BunProcess.cpp Resolved

@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.

Beyond the inline nit: e6a0c1d addresses the last two open items (NODE_NO_WARNINGS pin, re-capture comment wording). Also checked that the captureStackTraceForError extraction passing nullptr for callFrame is behavior-preserving — getFramesForCaller marks it UNUSED_PARAM (ErrorStackTrace.cpp:118), so Error.captureStackTrace semantics are unchanged. The candidate "detail appended in trace mode diverges from Node" was ruled out.

Extended reasoning...

The most recent commit e6a0c1d closes out the two nits from my previous round; every prior thread on this PR is now resolved. The one remaining inline finding this run is pre-existing (.code/.detail putDirect on user-supplied Errors) and same-class-adjacent to the .stack fix already made — worth noting but not a regression. The FormatStackTraceForJS.cpp refactor's only non-mechanical change is dropping the callFrame argument to getFramesForCaller; I verified that parameter is UNUSED_PARAM in the callee, so the JS-facing Error.captureStackTrace path is byte-for-byte equivalent. Given the C++ JSC-bindings surface (emit dispatch rewrite + captureStackTrace body extraction) and the number of iterations, leaving the final call to a human.

Comment thread src/jsc/bindings/BunProcess.cpp Resolved
Node's createWarningObject sets name/code/detail and captures the stack
only when the warning argument is a string; a caller-supplied Error is
emitted unmodified. The stack re-capture was already scoped to the
string branch; move the code/detail putDirect calls there too.
Comment thread src/js/internal/process/warning.ts Resolved
@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 5f701eb (build #75422) is green for this change: 284/286 jobs passed, and test/js/node/process/process-warning-output.test.ts plus the Node parallel test-process-*/test-util-deprecate tests pass on every lane.

The two red jobs are unrelated to this diff:

  • test/js/node/test/parallel/test-worker-message-port-transfer-terminate.js on debian x64-asan: pre-existing JSC getOwnPropertyDescriptor assertion during worker termination, also failing on main.
  • test/js/node/net/handle-leak.test.ts and test/regression/issue/20144/20144.test.ts on darwin-14 aarch64: known RSS-margin and SIGKILL/SIGINT flakes, both passed on retry elsewhere.

Ready for review.

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Status against current main (165dc9f), now that #31831 has landed and closed #9867.

test/js/node/process/process-warning-output.test.ts from this PR, run on main without this change: 4 pass, 5 fail.

Already on main via #31831: the (name:pid) [CODE] Name: message line, the detail line, a user 'warning' listener no longer suppressing the stderr report, NODE_NO_WARNINGS, and process.traceDeprecation = true at runtime. The stderr printer part of this PR now overlaps with createOnWarning in src/js/builtins/ProcessObjectInternals.ts.

Still failing on main. Node v26.3.0 behaves the way the tests expect in each case:

  • the hint line for a DeprecationWarning says --trace-warnings; Node says --trace-deprecation (createOnWarning hardcodes the flag)
  • process.emitWarning(err, { code, detail }) sets code and detail on the Error that was passed in; Node leaves a passed-in Error untouched
  • with --trace-warnings the printed stack starts with at emitWarning (unknown); Node starts at the caller
  • with --trace-deprecation the printed stack contains internal:util/deprecate frames; Node starts at the caller
  • with --throw-deprecation the uncaught error report shows the internal util/deprecate source (including the raw @undefined intrinsic) instead of the user's frame

So the stack re-capture in Process::emitWarning, the passed-in Error handling and the hint flag are still needed; the printer is not. This needs a rebase onto main that drops the printer hunk, and the Fixes #9867 line no longer applies since that issue is closed. Leaving open.

@robobun

robobun commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Checked this PR against main after #31831 merged. Method: debug build of main at 731aa92, then this PR's test/js/node/process/process-warning-output.test.ts run against main's sources with none of this PR's src/ changes. Result: 4 of 9 tests pass, 5 fail. So this PR is only partially superseded.

Landed in #31831 (the 4 passing tests):

  • The default stderr report. Main ports Node's onWarning as createOnWarning in src/js/builtins/ProcessObjectInternals.ts. It prints (node:PID) [CODE] Name: message, then the detail line, then the one-time trace hint. Main installs it as a real 'warning' listener (Process::installDefaultWarningListener in BunProcess.cpp), so a user 'warning' listener no longer suppresses the report. This is what fixed process.emitWarning is not showing the detail output in the terminal #9867.
  • NODE_NO_WARNINGS=1 delivers the event and only suppresses stderr.
  • process.traceDeprecation = true at runtime prints the stack.

Still open on main (the 5 failing tests):

  • Process::emitWarning still ignores ctor. Under --trace-warnings and --trace-deprecation the printed stack starts at at emitWarning (unknown) and includes the internal:util/deprecate frames. Under --throw-deprecation the uncaught error report still shows the builtin source with the raw @undefined intrinsic syntax. Node starts the stack at the user's call site. The captureStackTraceForError change in this PR is not on main.
  • code and detail are still written onto an Error instance passed to process.emitWarning (BunProcess.cpp, after the warning.isString() branch). Node only sets them on a warning it creates from a string.
  • The trace hint always says --trace-warnings. Node says --trace-deprecation for a DeprecationWarning. This PR's printer gets that right, main's createOnWarning does not.

The branch conflicts with main in BunProcess.cpp and FormatStackTraceForJS.cpp. A rebase can drop src/js/internal/process/warning.ts and the jsFunction_emitWarning change (main's printer covers them), and keep the ctor stack capture, the Error-instance change, and the hint flag (that one now belongs in createOnWarning).

Per-test result on main
(pass) NODE_NO_WARNINGS=1 still delivers the event but suppresses stderr
(fail) --throw-deprecation does not leak internal builtin source
(pass) process.emitWarning with code and detail formats Node-style
(fail) default report uses Node's one-line (name:pid) [CODE] form   (hint line names --trace-warnings, test expects --trace-deprecation)
(pass) a user 'warning' listener does not suppress the stderr report
(fail) --trace-deprecation prints the warning stack instead of the hint   (stack contains internal:util/deprecate)
(fail) passing an Error instance leaves it unmutated even when ctor/code/detail are given
(fail) --trace-warnings prints the warning stack for non-deprecation warnings   (stack contains "at emitWarning")
(pass) process.traceDeprecation = true enables stack printing at runtime

@robobun

robobun commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

#48213 replaces the part of this PR that is about the stack of a warning. It is based on current main, and it captures the stack through the body of Error.captureStackTrace with ctor.

The printer of this PR is on main since #31831. What is left here and in no other PR is the hint under a deprecation: it names --trace-warnings, and Node names --trace-deprecation (src/js/builtins/ProcessObjectInternals.ts:766).

@robobun

robobun commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

Status on main at 2596ac5, from debug builds.

This PR's process-warning-output.test.ts has 9 tests.

build pass
main 5
#48213 7
#48213 with #47388 merged in by hand 8

#48213 covers the two tests for the printed stack (--trace-deprecation and --trace-warnings). #47388 covers the passed-in Error. The --throw-deprecation test passes on main now.

The one test that is left is the hint under a deprecation. Main names --trace-warnings there, and Node names --trace-deprecation. With that change on top of the two PRs, all 9 pass.

The hint change is written and tested, but it is not a PR yet. #48120 rewrites the lines that build the hint: createOnWarning gets a second text under src/js/ahead-of-time. I will open the change as a small PR of its own on that layout when #48120 is on main, and close this PR then.

@robobun

robobun commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Closing. Each part of this PR now lives in another place.

This PR's process-warning-output.test.ts has 9 tests. I ran it on debug builds. At 2596ac5, main passes 5, #48213 passes 7, #48213 with #47388 merged in by hand passes 8, and all 9 pass with the hint change on top. On today's main (98cda0d) with #48441 alone, 6 pass. The other 3 are the tests that #48213 and #47388 cover.

One behaviour of this PR is in none of them, on purpose. Its printer used Error.prototype.toString when warning.toString() throws. Node v26.3.0 lets that exception through, and so does main.

@robobun robobun closed this Nov 10, 2026
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.

process.emitWarning is not showing the detail output in the terminal

1 participant