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
6 changes: 6 additions & 0 deletions src/jsc/bindings/ZigException.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ class V8StackTraceIterator {
bool isConstructor = false;
bool isGlobalCode = false;
bool isAsync = false;
// Printed as "name (url)" or "<anonymous> (url)", not as a bare "url".
bool isFunction = false;
};

WTF::StringView stack;
Expand Down Expand Up @@ -400,6 +402,8 @@ class V8StackTraceIterator {
functionName = functionName.substring(4);
}

frame.isFunction = !functionName.isEmpty();

if (functionName == "<anonymous>"_s) {
functionName = StringView();
}
Expand Down Expand Up @@ -619,6 +623,8 @@ static void fromErrorInstance(ZigException& except, JSC::JSGlobalObject* global,
current.code_type = ZigStackFrameCodeConstructor;
} else if (frame.isGlobalCode) {
current.code_type = ZigStackFrameCodeGlobal;
} else if (frame.isFunction) {
current.code_type = ZigStackFrameCodeFunction;
}

except.stack.frames_len += 1;
Expand Down
5 changes: 2 additions & 3 deletions test/cli/inspect/inspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,10 +596,9 @@ test("error.stack doesnt lose frames", () => {
at middle (<dir>/inspect.test.ts:<num>:<num>)
at IGNORE_ME_BEFORE_THIS_LINE (<dir>/inspect.test.ts:<num>:<num>)
at accessErrorStackProperty (<dir>/inspect.test.ts:<num>:<num>)
at <dir>/inspect.test.ts:<num>:<num>
at <anonymous> (<dir>/inspect.test.ts:<num>:<num>)
"
`);

// We allow it to differ by the existence of <anonymous> as a string. But that's it.
expect(no.split("\n").slice(0, -2).join("\n").trim()).toBe(yes.split("\n").slice(0, -2).join("\n").trim());
expect(yes).toBe(no);
});
102 changes: 101 additions & 1 deletion test/js/bun/test/stack.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { $ } from "bun";
import { expect, test } from "bun:test";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, bunRun, normalizeBunSnapshot } from "harness";
import { join } from "node:path";

Expand Down Expand Up @@ -149,3 +149,103 @@ test("Async functions frame should be included in stack trace", async () => {
at async <anonymous> (file:NN:NN)"
`);
});

// Once error.stack has been read (or assigned), JSC no longer has the frames of
// the error, and Bun.inspect / console.error / the uncaught error output print
// the frames parsed back out of that string instead.
describe("printing the frames parsed back out of error.stack", () => {
const printedFrames = (text: string) =>
text
.split("\n")
.map(line => line.trim())
.filter(line => line.startsWith("at "));

// "at async outer (/x.js:1:2)" -> "at async outer": where the frames point is
// not what is being tested here.
const withoutLocation = (line: string) => line.replace(/ \(.*$/, "");

test("async, <anonymous>, new and bare frames print as they do in error.stack", () => {
const err = new Error("boom");
err.stack = [
"Error: boom",
" at async fetchUser (/fake/api.js:10:9)",
" at async <anonymous> (/fake/app.js:4:3)",
" at <anonymous> (/fake/app.js:9:1)",
" at new Client (/fake/client.js:2:11)",
" at run (/fake/main.js:7:5)",
" at global code (/fake/main.js:12:1)",
" at unknown",
].join("\n");

const expected = [
"at async fetchUser (/fake/api.js:10:9)",
"at async <anonymous> (/fake/app.js:4:3)",
"at <anonymous> (/fake/app.js:9:1)",
"at new Client (/fake/client.js:2:11)",
"at run (/fake/main.js:7:5)",
"at /fake/main.js:12:1",
expect.stringMatching(/^at unknown\b/),
];
expect(printedFrames(Bun.inspect(err, { colors: false }))).toEqual(expected);
expect(printedFrames(Bun.stripANSI(Bun.inspect(err, { colors: true })))).toEqual(expected);
});

test("Bun.inspect prints the same frames before and after error.stack has been read", async () => {
async function inner() {
await 1;
throw new Error("boom");
}
async function outer() {
await inner();
}
const err: Error = await (async () => {
await outer();
})().catch(e => e);

const ownFrames = (text: string) =>
printedFrames(text)
.filter(line => line.includes("stack.test.ts"))
.map(withoutLocation);
const before = ownFrames(Bun.inspect(err));
const stack = ownFrames(err.stack!);
const after = ownFrames(Bun.inspect(err));

expect(before).toEqual(["at inner", "at async outer", "at async <anonymous>"]);
expect({ stack, after }).toEqual({ stack: before, after: before });
});

test("console.error and the unhandled rejection output keep the async frames", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
async function inner() {
await 1;
throw new Error("boom");
}
async function outer() {
await inner();
}
(async () => {
await outer();
})().catch(e => {
e.stack; // e.g. a logger reading it
console.error(e);
throw e;
});
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

// Printed once by console.error(e) and once as an unhandled rejection.
const expected = ["at inner", "at async outer", "at async <anonymous>"];
expect(printedFrames(stderr).map(withoutLocation)).toEqual([...expected, ...expected]);
expect(stdout).toBe("");
expect(exitCode).toBe(1);
});
});