Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
368f8284ea Fix process.emitWarning to use Node.js-compatible formatting
This change makes Bun's warning output much less scary by matching Node.js behavior.

Changes:
- Add --trace-warnings CLI flag support
- Format warnings as: (bun:PID) WarningName: message
- Show stack traces only when --trace-warnings is passed
- Show helpful hint about --trace-warnings flag (only once per process)
- Warnings go to stderr (not console.warn which showed full error objects)

Before:
  process.emitWarning() would dump full error object with stack trace,
  making every warning look like a critical error

After:
  - Without --trace-warnings: Shows minimal format with helpful hint
  - With --trace-warnings: Shows stack trace like Node.js

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 08:35:58 +00:00
4 changed files with 236 additions and 2 deletions

View File

@@ -1450,6 +1450,65 @@ Process::~Process()
}
extern "C" bool Bun__NODE_NO_WARNINGS();
extern "C" bool Bun__NODE_TRACE_WARNINGS();
// Track whether we've shown the hint message about --trace-warnings
static bool hasShownTraceWarningsHint = false;
static void printWarningToStderr(JSC::JSGlobalObject* globalObject, JSValue errorInstance, bool traceWarnings)
{
auto& vm = JSC::getVM(globalObject);
// Get the warning name (e.g., "Warning", "DeprecationWarning")
auto warningName = errorInstance.get(globalObject, vm.propertyNames->name);
String nameStr = "Warning"_s;
if (warningName.isString()) {
nameStr = warningName.getString(globalObject);
}
// Get the warning message
auto warningMessage = errorInstance.get(globalObject, vm.propertyNames->message);
String messageStr = ""_s;
if (warningMessage.isString()) {
messageStr = warningMessage.getString(globalObject);
}
// Get PID
pid_t pid = getpid();
// Format: (bun:PID) WarningName: message
auto firstLine = makeString("(bun:"_s, String::number(pid), ") "_s, nameStr);
if (!messageStr.isEmpty()) {
firstLine = makeString(firstLine, ": "_s, messageStr);
}
fprintf(stderr, "%s\n", firstLine.utf8().data());
if (traceWarnings) {
// Show stack trace
auto stackValue = errorInstance.get(globalObject, vm.propertyNames->stack);
if (stackValue.isString()) {
String stackStr = stackValue.getString(globalObject);
// The stack typically includes the error name and message as first line,
// so we need to extract just the stack frames
auto lines = stackStr.split('\n');
bool firstLine = true;
for (const auto& line : lines) {
// Skip the first line if it looks like an error message line
if (firstLine && (line.startsWith(nameStr) || line.contains(messageStr))) {
firstLine = false;
continue;
}
firstLine = false;
fprintf(stderr, "%s\n", line.utf8().data());
}
}
} else if (!hasShownTraceWarningsHint) {
// Show the hint about --trace-warnings only once per process
fprintf(stderr, "(Use `bun --trace-warnings ...` to show where the warning was created)\n");
hasShownTraceWarningsHint = true;
}
}
JSC_DEFINE_HOST_FUNCTION(jsFunction_emitWarning, (JSC::JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
@@ -1466,8 +1525,8 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_emitWarning, (JSC::JSGlobalObject * lexicalG
process->wrapped().emit(ident, args);
return JSValue::encode(jsUndefined());
} else if (!Bun__NODE_NO_WARNINGS()) {
auto jsArgs = JSValue::encode(value);
Bun__ConsoleObject__messageWithTypeAndLevel(reinterpret_cast<Bun::ConsoleObject*>(globalObject->consoleClient().get())->m_client, static_cast<uint32_t>(MessageType::Log), static_cast<uint32_t>(MessageLevel::Warning), globalObject, &jsArgs, 1);
// Use Node.js-compatible warning formatting
printWarningToStderr(globalObject, value, Bun__NODE_TRACE_WARNINGS());
RETURN_IF_EXCEPTION(scope, {});
}
return JSValue::encode(jsUndefined());

View File

@@ -342,6 +342,11 @@ pub export fn Bun__NODE_NO_WARNINGS() bool {
return bun.feature_flag.NODE_NO_WARNINGS.get();
}
extern var Bun__Node__ProcessTraceWarnings: bool;
pub export fn Bun__NODE_TRACE_WARNINGS() bool {
return Bun__Node__ProcessTraceWarnings;
}
pub export fn Bun__suppressCrashOnProcessKillSelfIfDesired() void {
if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_ON_PROCESS_KILL_SELF.get()) {
bun.crash_handler.suppressReporting();

View File

@@ -102,6 +102,7 @@ pub const runtime_params_ = [_]ParamType{
clap.parseParam("--expose-gc Expose gc() on the global object. Has no effect on Bun.gc().") catch unreachable,
clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable,
clap.parseParam("--throw-deprecation Determine whether or not deprecation warnings result in errors.") catch unreachable,
clap.parseParam("--trace-warnings Show stack traces on process warnings") catch unreachable,
clap.parseParam("--title <STR> Set the process title") catch unreachable,
clap.parseParam("--zero-fill-buffers Boolean to force Buffer.allocUnsafe(size) to be zero-filled.") catch unreachable,
clap.parseParam("--use-system-ca Use the system's trusted certificate authorities") catch unreachable,
@@ -784,6 +785,9 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
if (args.flag("--throw-deprecation")) {
Bun__Node__ProcessThrowDeprecation = true;
}
if (args.flag("--trace-warnings")) {
Bun__Node__ProcessTraceWarnings = true;
}
if (args.option("--title")) |title| {
CLI.Bun__Node__ProcessTitle = title;
}
@@ -1319,6 +1323,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
export var Bun__Node__ZeroFillBuffers = false;
export var Bun__Node__ProcessNoDeprecation = false;
export var Bun__Node__ProcessThrowDeprecation = false;
export var Bun__Node__ProcessTraceWarnings = false;
pub const BunCAStore = enum(u8) { bundled, openssl, system };
pub export var Bun__Node__CAStore: BunCAStore = .bundled;

View File

@@ -0,0 +1,165 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("process.emitWarning without --trace-warnings shows minimal format", async () => {
using dir = tempDir("trace-warnings-test", {
"test.js": `process.emitWarning('test warning');`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// Should show minimal format with PID
expect(stderr).toMatch(/\(bun:\d+\) Warning: test warning/);
// Should show the hint about --trace-warnings
expect(stderr).toContain("(Use `bun --trace-warnings ...` to show where the warning was created)");
// Should NOT show stack trace
expect(stderr).not.toContain("at ");
expect(exitCode).toBe(0);
});
test("process.emitWarning with --trace-warnings shows stack trace", async () => {
using dir = tempDir("trace-warnings-test", {
"test.js": `process.emitWarning('test warning');`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--trace-warnings", "test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// Should show minimal format with PID
expect(stderr).toMatch(/\(bun:\d+\) Warning: test warning/);
// Should show stack trace
expect(stderr).toContain("at ");
// Should NOT show the hint when --trace-warnings is used
expect(stderr).not.toContain("(Use `bun --trace-warnings ...` to show where the warning was created)");
expect(exitCode).toBe(0);
});
test("process.emitWarning with custom type", async () => {
using dir = tempDir("trace-warnings-test", {
"test.js": `process.emitWarning('custom message', 'CustomWarning');`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// Should show custom warning type
expect(stderr).toMatch(/\(bun:\d+\) CustomWarning: custom message/);
expect(exitCode).toBe(0);
});
test("multiple warnings show hint only once", async () => {
using dir = tempDir("trace-warnings-test", {
"test.js": `
process.emitWarning('warning 1');
process.emitWarning('warning 2');
process.emitWarning('warning 3');
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// Should show all three warnings
expect(stderr).toContain("Warning: warning 1");
expect(stderr).toContain("Warning: warning 2");
expect(stderr).toContain("Warning: warning 3");
// Should show the hint only once
const hintCount = (stderr.match(/Use `bun --trace-warnings/g) || []).length;
expect(hintCount).toBe(1);
expect(exitCode).toBe(0);
});
test("DeprecationWarning is shown by default", async () => {
using dir = tempDir("trace-warnings-test", {
"test.js": `process.emitWarning('deprecated API', 'DeprecationWarning');`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toMatch(/\(bun:\d+\) DeprecationWarning: deprecated API/);
expect(exitCode).toBe(0);
});
test("NODE_NO_WARNINGS=1 suppresses warnings", async () => {
using dir = tempDir("trace-warnings-test", {
"test.js": `process.emitWarning('test warning');`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: { ...bunEnv, NODE_NO_WARNINGS: "1" },
cwd: String(dir),
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// Should NOT show any warning
expect(stderr).not.toContain("Warning:");
expect(stderr).not.toContain("test warning");
expect(exitCode).toBe(0);
});
test("process.emitWarning with code and detail", async () => {
using dir = tempDir("trace-warnings-test", {
"test.js": `
process.emitWarning('test warning', {
type: 'CustomWarning',
code: 'CODE001',
detail: 'Additional details here'
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toMatch(/\(bun:\d+\) CustomWarning: test warning/);
expect(exitCode).toBe(0);
});