Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5507,7 +5507,7 @@ impl VirtualMachine {

if frames.len() > 1 {
for i in 0..frames.len() {
if i == top || frames[i].position.is_invalid() {
if i == top || frames[i].position.is_invalid() || frames[i].remapped {
continue;
}
let source_url = frames[i].source_url.to_utf8();
Expand Down
22 changes: 15 additions & 7 deletions src/jsc/bindings/ZigException.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ class V8StackTraceIterator {
if (openingParentheses > closingParentheses)
openingParentheses = WTF::notFound;

StringView lineInner;
StringView functionName;

if (openingParentheses == WTF::notFound || closingParentheses == WTF::notFound) {
// Special case: "unknown" frames don't have parentheses but are valid
// These appear in stack traces from certain error paths
Expand All @@ -305,13 +308,20 @@ class V8StackTraceIterator {
return true;
}

// For any other frame without parentheses, terminate parsing as before
offset = stack.length();
return false;
// V8/Node format for frames without a function name:
// at /path/to/file.js:1:2
// at async /path/to/file.js:1:2
Comment thread
robobun marked this conversation as resolved.
Outdated
lineInner = line;
if (lineInner.startsWith("async "_s)) {
frame.isAsync = true;
lineInner = lineInner.substring(6);
}
functionName = StringView();
} else {
lineInner = StringView_slice(line, openingParentheses + 1, closingParentheses);
functionName = line.substring(0, openingParentheses - 1);
}

auto lineInner = StringView_slice(line, openingParentheses + 1, closingParentheses);

{
auto marker1 = 0;
auto marker2 = lineInner.find(':', marker1);
Expand Down Expand Up @@ -383,8 +393,6 @@ class V8StackTraceIterator {
}
done_block:

StringView functionName = line.substring(0, openingParentheses - 1);

if (functionName == "global code"_s) {
functionName = StringView();
frame.isGlobalCode = true;
Expand Down
120 changes: 120 additions & 0 deletions test/regression/issue/15859.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";

// https://github.com/oven-sh/bun/issues/15859
//
// Reading `error.stack` before rethrowing caused the uncaught-exception printer
// to show wrong line numbers for non-top frames (double source-mapped) and to
// drop frames without a function name.

const fixture = `import * as i1 from "util";
import * as i2 from "util";
import * as i3 from "util";
function err() {
throw new Error()
};
function f1(){
err()
}
function f2(){

}
try {
f1();
} catch (error: any) {
let x = error.stack
throw error
}
`;

test("uncaught exception frames are not double source-mapped after reading error.stack", async () => {
using dir = tempDir("issue-15859", {
"test.ts": fixture,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toBe("");
// f1's body calls err() on line 8. Before the fix this printed line 13
// (source-mapped twice).
expect(stderr).toContain("at f1 ");
expect(stderr).toMatch(/at f1 \(.*test\.ts:8:5\)/);
expect(stderr).not.toMatch(/at f1 \(.*test\.ts:13:/);
// err() throws on line 5; this frame was already correct (top frame).
expect(stderr).toMatch(/at err \(.*test\.ts:5:/);
expect(exitCode).toBe(1);
});

test("uncaught exception printer keeps the anonymous top-level frame after reading error.stack", async () => {
using dir = tempDir("issue-15859-anon", {
"test.ts": fixture,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toBe("");
// The try block calls f1() on line 14 from module scope (no function name).
// Before the fix this frame was dropped entirely because the .stack string
// parser could not handle "at <url>:<line>:<col>" frames.
expect(stderr).toMatch(/at .*test\.ts:14:5/);
expect(exitCode).toBe(1);
});

test("uncaught exception frames match error.stack after reading it", async () => {
using dir = tempDir("issue-15859-match", {
"test.ts": `import * as i1 from "util";
import * as i2 from "util";
import * as i3 from "util";
function err() {
throw new Error()
};
function f1(){
err()
}
function f2(){

}
try {
f1();
} catch (error: any) {
console.log(error.stack)
throw error
}
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const extract = (s: string) => [...s.matchAll(/test\.ts:(\d+):(\d+)/g)].map(m => `${m[1]}:${m[2]}`);

const stackPositions = extract(stdout);
const printedPositions = extract(stderr);

expect(stackPositions.length).toBeGreaterThanOrEqual(3);
expect(printedPositions).toEqual(stackPositions);
expect(exitCode).toBe(1);
});
Loading