import { nativeFrameForTesting } from "bun:internal-for-testing"; import { noInline } from "bun:jsc"; import { afterEach, expect, mock, test } from "bun:test"; const origPrepareStackTrace = Error.prepareStackTrace; afterEach(() => { Error.prepareStackTrace = origPrepareStackTrace; }); test("Regular .stack", () => { var err; class Foo { constructor() { err = new Error("wat"); } } new Foo(); expect(err.stack).toMatch(/at new Foo/); }); test("throw inside Error.prepareStackTrace doesnt crash", () => { Error.prepareStackTrace = function (err, stack) { Error.prepareStackTrace = null; throw new Error("wat"); }; expect(() => new Error().stack).toThrow("wat"); }); test("capture stack trace", () => { function f1() { f2(); } function f2() { f3(); } function f3() { logErrorStackTrace(); } function logErrorStackTrace() { let error = {}; Error.captureStackTrace(error); expect(error.stack !== undefined).toBe(true); } f1(); }); test("capture stack trace with message", () => { function f1() { f2(); } function f2() { f3(); } function f3() { logErrorStackTrace(); } function logErrorStackTrace() { let e1 = { message: "bad error!" }; Error.captureStackTrace(e1); expect(e1.message === "bad error!").toBe(true); let e2 = new Error("bad error!"); Error.captureStackTrace(e2); expect(e2.message === "bad error!").toBe(true); } f1(); }); test("capture stack trace with constructor", () => { class S { constructor() { captureStackTrace(); } } function captureStackTrace() { let e1 = {}; Error.captureStackTrace(e1); expect(e1.stack.split("\n")[2].includes("new S")).toBe(true); } let s = new S(); }); test("capture stack trace limit", () => { function f1() { f2(); } function f2() { f3(); } function f3() { f4(); } function f4() { f5(); } function f5() { f6(); } function f6() { f7(); } function f7() { f8(); } function f8() { f9(); } function f9() { f10(); } function f10() { captureStackTrace(); } var originalLimit = Error.stackTraceLimit; function captureStackTrace() { let e1 = {}; Error.captureStackTrace(e1); expect(e1.stack.split("\n").length).toBe(11); let e2 = new Error(); Error.captureStackTrace(e2); expect(e2.stack.split("\n").length).toBe(11); let e3 = {}; Error.stackTraceLimit = 4; Error.captureStackTrace(e3); expect(e3.stack.split("\n").length).toBe(5); let e4 = new Error(); Error.captureStackTrace(e4); expect(e4.stack.split("\n").length).toBe(5); let e5 = { stackTraceLimit: 2 }; Error.captureStackTrace(e5); expect(e5.stack.split("\n").length).toBe(5); let e6 = {}; Error.stackTraceLimit = Infinity; Error.captureStackTrace(e6); expect(e6.stack.split("\n").length).toBe(13); } try { f1(); } finally { Error.stackTraceLimit = originalLimit; } }); test("prepare stack trace", () => { function f1() { f2(); } function f2() { let e = {}; let prevPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, stack) => { return "custom stack trace"; }; Error.captureStackTrace(e); expect(e.stack).toBe("custom stack trace"); Error.prepareStackTrace = prevPrepareStackTrace; f3(); } function f3() { let e = { message: "bad error!" }; let prevPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, s) => { expect(e.message === "bad error!").toBe(true); expect(s.length).toBe(4); }; Error.stackTraceLimit = 10; Error.captureStackTrace(e); expect(e.stack === undefined).toBe(true); Error.prepareStackTrace = prevPrepareStackTrace; } f1(); }); test("capture stack trace second argument", () => { function f0() { let s = new S(); } class S { constructor() { f1(); } } function f1() { f2(); } function f2() { f3(); } function f3() { f4(); } function f4() { f5(); } function f5() { f6(); } function f6() { let e = { message: "bad error!" }; Error.captureStackTrace(e); expect(e.stack.split("\n")[1].includes("at f6")).toBe(true); expect(e.stack.split("\n")[2].includes("at f5")).toBe(true); let e2 = {}; Error.captureStackTrace(e2, f3); expect(e2.stack.split("\n")[1].includes("at f2")).toBe(true); expect(e2.stack.split("\n")[2].includes("at f1")).toBe(true); let e3 = {}; Error.captureStackTrace(e3, f9); expect(e3.stack.split("\n").length).toBe(1); let e4 = { message: "exclude constructor!" }; Error.captureStackTrace(e4, S.constructor); expect(e4.stack.split("\n").length).toBe(1); let e5 = { message: "actually exclude constructor!" }; Error.captureStackTrace(e5, S); expect(e5.stack.split("\n")[1].includes("at f0")).toBe(true); } function f9() { // nothing } f0(); }); test("capture stack trace edge cases", () => { let e1 = {}; Error.captureStackTrace(e1, null); expect(e1.stack !== undefined).toBe(true); let e2 = {}; Error.captureStackTrace(e2, undefined); expect(e2.stack !== undefined).toBe(true); let e3 = {}; Error.captureStackTrace(e3, 1); expect(e3.stack !== undefined).toBe(true); let e4 = {}; Error.captureStackTrace(e4, "foo"); expect(e4.stack !== undefined).toBe(true); let e5 = {}; Error.captureStackTrace(e5, {}); expect(e5.stack !== undefined).toBe(true); expect(Error.captureStackTrace({})).toBe(undefined); expect(Error.captureStackTrace({}, () => {})).toBe(undefined); expect(Error.captureStackTrace({}, undefined)).toBe(undefined); expect(Error.captureStackTrace({}, null)).toBe(undefined); expect(Error.captureStackTrace({}, 1)).toBe(undefined); expect(Error.captureStackTrace({}, "foo")).toBe(undefined); expect(Error.captureStackTrace({}, {})).toBe(undefined); expect(Error.captureStackTrace({}, [])).toBe(undefined); expect(Error.captureStackTrace({}, true)).toBe(undefined); }); test("prepare stack trace call sites", () => { function f1() { f2(); } function f2() { f3(); } function f3() { let e = { message: "bad error!" }; // let e = new Error("bad error!"); let prevPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, s) => { expect(s[0].getThis !== undefined).toBe(true); expect(s[0].getTypeName !== undefined).toBe(true); expect(s[0].getFunction !== undefined).toBe(true); expect(s[0].getFunctionName !== undefined).toBe(true); expect(s[0].getMethodName !== undefined).toBe(true); expect(s[0].getFileName !== undefined).toBe(true); expect(s[0].getLineNumber !== undefined).toBe(true); expect(s[0].getColumnNumber !== undefined).toBe(true); expect(s[0].getEvalOrigin !== undefined).toBe(true); expect(s[0].isToplevel !== undefined).toBe(true); expect(s[0].isEval !== undefined).toBe(true); expect(s[0].isNative !== undefined).toBe(true); expect(s[0].isConstructor !== undefined).toBe(true); expect(s[0].isAsync !== undefined).toBe(true); expect(s[0].isPromiseAll !== undefined).toBe(true); expect(s[0].getPromiseIndex !== undefined).toBe(true); }; Error.captureStackTrace(e); expect(e.stack === undefined).toBe(true); Error.prepareStackTrace = prevPrepareStackTrace; } f1(); }); test("sanity check", () => { function f1() { f2(); } function f2() { f3(); } function f3() { let e = new Error("bad error!"); let prevPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, s) => { // getThis returns undefined in strict mode expect(s[0].getThis()).toBe(undefined); expect(s[0].getTypeName()).toBe("undefined"); // getFunction returns undefined in strict mode expect(s[0].getFunction()).toBe(undefined); expect(s[0].getFunctionName()).toBe("f3"); expect(s[0].getMethodName()).toBe("f3"); expect(typeof s[0].getLineNumber()).toBe("number"); expect(typeof s[0].getColumnNumber()).toBe("number"); expect(s[0].getFileName().includes("capture-stack-trace.test.js")).toBe(true); expect(s[0].getEvalOrigin()).toBe(undefined); expect(s[0].isToplevel()).toBe(true); expect(s[0].isEval()).toBe(false); expect(s[0].isNative()).toBe(false); expect(s[0].isConstructor()).toBe(false); expect(s[0].isAsync()).toBe(false); expect(s[0].isPromiseAll()).toBe(false); expect(s[0].getPromiseIndex()).toBe(null); }; Error.captureStackTrace(e); expect(e.stack === undefined).toBe(true); Error.prepareStackTrace = prevPrepareStackTrace; } f1(); }); test("CallFrame isEval works as expected", () => { let prevPrepareStackTrace = Error.prepareStackTrace; let name, fn; Error.prepareStackTrace = (e, s) => { return s; }; name = "f1"; const stack = eval(`(function ${name}() { return new Error().stack; })()`); Error.prepareStackTrace = prevPrepareStackTrace; // TODO: 0 and 1 should both return true here. expect(stack[1].isEval()).toBe(true); expect(stack[0].getFunctionName()).toBe(name); }); test("CallFrame isTopLevel returns false for Function constructor", () => { let prevPrepareStackTrace = Error.prepareStackTrace; const sloppyFn = new Function("let e=new Error();Error.captureStackTrace(e);return e.stack"); sloppyFn.displayName = "sloppyFnWow"; noInline(sloppyFn); const that = {}; Error.prepareStackTrace = (e, s) => { expect(s[0].getFunctionName()).toBe(sloppyFn.displayName); expect(s[0].getFunction()).toBe(sloppyFn); expect(s[0].isToplevel()).toBe(false); expect(s[0].isEval()).toBe(false); // Strict-mode functions shouldn't have getThis or getFunction // available. expect(s[1].getThis()).toBe(undefined); expect(s[1].getFunction()).toBe(undefined); }; sloppyFn.call(that); Error.prepareStackTrace = prevPrepareStackTrace; }); test("CallFrame.p.getThisgetFunction: strict/sloppy mode interaction", () => { let prevPrepareStackTrace = Error.prepareStackTrace; const strictFn = new Function('"use strict";let e=new Error();Error.captureStackTrace(e);return e.stack'); const sloppyFn = new Function("x", "x()"); const that = {}; Error.prepareStackTrace = (e, s) => { // The first strict mode function encounted during stack unwinding // stops subsequent frames from having getThis\getFunction. for (const t of s) { expect(t.getThis()).toBe(undefined); expect(t.getFunction()).toBe(undefined); } }; sloppyFn.call(that, strictFn); Error.prepareStackTrace = prevPrepareStackTrace; }); test("CallFrame.p.isConstructor", () => { let prevPrepareStackTrace = Error.prepareStackTrace; class C { constructor() { Error.captureStackTrace(new Error("")); } } Error.prepareStackTrace = (e, s) => { expect(s[0].isConstructor()).toBe(true); // TODO: should be false: this is an instance of C expect(s[0].isToplevel()).toBe(true); // TODO: should return the class name // expect(s[0].getTypeName()).toBe('C'); expect(s[1].isConstructor()).toBe(false); expect(s[1].isToplevel()).toBe(true); }; new C(); Error.prepareStackTrace = prevPrepareStackTrace; }); test("CallFrame.p.isNative", () => { let prevPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, s) => { expect(s[0].isNative()).toBe(false); expect(s[1].isNative()).toBe(true); expect(s[2].isNative()).toBe(false); }; nativeFrameForTesting(() => { const err = new Error(""); Error.captureStackTrace(err); return 0; }); Error.prepareStackTrace = prevPrepareStackTrace; }); test("return non-strings from Error.prepareStackTrace", () => { // This behavior is allowed by V8 and used by the node-depd npm package. let prevPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, s) => s; const e = new Error(); Error.captureStackTrace(e); expect(Array.isArray(e.stack)).toBe(true); Error.prepareStackTrace = prevPrepareStackTrace; }); test("CallFrame.p.toString", () => { let prevPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, s) => s; const e = new Error(); Error.captureStackTrace(e); expect(e.stack[0].toString().includes("")).toBe(true); }); // TODO: line numbers are wrong in a release build test("err.stack should invoke prepareStackTrace", () => { var lineNumber = -1; var functionName = ""; var parentLineNumber = -1; function functionWithAName() { // This is V8's behavior. let prevPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, s) => { lineNumber = s[0].getLineNumber(); functionName = s[0].getFunctionName(); parentLineNumber = s[1].getLineNumber(); expect(s[0].getFileName().includes("capture-stack-trace.test.js")).toBe(true); expect(s[1].getFileName().includes("capture-stack-trace.test.js")).toBe(true); }; const e = new Error(); e.stack; Error.prepareStackTrace = prevPrepareStackTrace; } functionWithAName(); expect(functionName).toBe("functionWithAName"); expect(lineNumber).toBe(518); expect(parentLineNumber).toBe(523); }); test("Error.prepareStackTrace inside a node:vm works", () => { const { runInNewContext } = require("node:vm"); Error.prepareStackTrace = null; const result = runInNewContext( ` Error.prepareStackTrace = (err, stack) => { if (typeof err.stack !== "string") { throw new Error("err.stack is not a string"); } return "custom stack trace"; }; const err = new Error(); err.stack; `, ); expect(result).toBe("custom stack trace"); expect(Error.prepareStackTrace).toBeNull(); }); test("Error.captureStackTrace inside error constructor works", () => { class ExtendedError extends Error { constructor() { super(); Error.captureStackTrace(this, ExtendedError); } } class AnotherError extends ExtendedError {} expect(() => { throw new AnotherError(); }).toThrow(); }); import "harness"; import { join } from "path"; test("Error.prepareStackTrace has a default implementation which behaves the same as being unset", () => { expect([join(import.meta.dirname, "error-prepare-stack-default-fixture.js")]).toRun(); }); test("Error.prepareStackTrace returns a CallSite object", () => { Error.prepareStackTrace = function (err, stack) { return stack; }; const error = new Error(); expect(error.stack[0]).not.toBeString(); expect(error.stack[0][Symbol.toStringTag]).toBe("CallSite"); }); test("Error.captureStackTrace updates the stack property each call, even if Error.prepareStackTrace is set", () => { const prevPrepareStackTrace = Error.prepareStackTrace; var didCallPrepareStackTrace = false; let error = new Error(); const firstStack = error.stack; Error.prepareStackTrace = function (err, stack) { expect(err.stack).not.toBe(firstStack); didCallPrepareStackTrace = true; return stack; }; function outer() { inner(); } function inner() { Error.captureStackTrace(error); } outer(); const secondStack = error.stack; expect(firstStack).not.toBe(secondStack); expect(firstStack).toBeString(); expect(firstStack).not.toContain("outer"); expect(firstStack).not.toContain("inner"); expect(didCallPrepareStackTrace).toBe(true); expect(secondStack.find(a => a.getFunctionName() === "outer")).toBeTruthy(); expect(secondStack.find(a => a.getFunctionName() === "inner")).toBeTruthy(); Error.prepareStackTrace = prevPrepareStackTrace; }); test("Error.captureStackTrace updates the stack property each call", () => { let error = new Error(); const firstStack = error.stack; function outer() { inner(); } function inner() { Error.captureStackTrace(error); } outer(); const secondStack = error.stack; expect(firstStack).not.toBe(secondStack); expect(firstStack.length).toBeLessThan(secondStack.length); expect(firstStack).not.toContain("outer"); expect(firstStack).not.toContain("inner"); expect(secondStack).toContain("outer"); expect(secondStack).toContain("inner"); }); test("calling .stack later uses the stored StackTrace", function hey() { let error = new Error(); let stack; function outer() { inner(); } function inner() { stack = error.stack; } outer(); expect(stack).not.toContain("outer"); expect(stack).not.toContain("inner"); expect(stack).toContain("hey"); }); test("calling .stack on a non-materialized Error updates the stack properly", function hey() { let error = new Error(); let stack; function outer() { inner(); } function inner() { stack = error.stack; } function wrapped() { Error.captureStackTrace(error); } wrapped(); outer(); expect(stack).not.toContain("outer"); expect(stack).not.toContain("inner"); expect(stack).toContain("hey"); expect(stack).toContain("wrapped"); }); test("Error.prepareStackTrace on an array with non-CallSite objects doesn't crash", () => { const result = Error.prepareStackTrace(new Error("ok"), [{ a: 1 }, { b: 2 }, { c: 3 }]); expect(result).toBe("Error: ok\n at [object Object]\n at [object Object]\n at [object Object]"); }); test("Error.prepareStackTrace calls toString()", () => { const result = Error.prepareStackTrace(new Error("ok"), [ { a: 1 }, { b: 2 }, { c: 3, toString() { return "potato"; }, }, ]); expect(result).toBe("Error: ok\n at [object Object]\n at [object Object]\n at potato"); }); test("Error.prepareStackTrace propagates exceptions", () => { expect(() => Error.prepareStackTrace(new Error("ok"), [ { a: 1 }, { b: 2 }, { c: 3, toString() { throw new Error("hi"); }, }, ]), ).toThrow("hi"); }); test("CallFrame.p.getScriptNameOrSourceURL inside eval", () => { let prevPrepareStackTrace = Error.prepareStackTrace; const prepare = mock((e, s) => { expect(s[0].getScriptNameOrSourceURL()).toBe("https://zombo.com/welcome-to-zombo.js"); expect(s[1].getScriptNameOrSourceURL()).toBe("https://zombo.com/welcome-to-zombo.js"); expect(s[2].getScriptNameOrSourceURL()).toBe("[native code]"); expect(s[3].getScriptNameOrSourceURL()).toBe(import.meta.path); expect(s[4].getScriptNameOrSourceURL()).toBe(import.meta.path); }); Error.prepareStackTrace = prepare; let evalScript = `(function() { throw new Error("bad error!"); })() //# sourceURL=https://zombo.com/welcome-to-zombo.js`; try { function insideAFunction() { eval(evalScript); } insideAFunction(); } catch (e) { e.stack; } Error.prepareStackTrace = prevPrepareStackTrace; expect(prepare).toHaveBeenCalledTimes(1); }); test("CallFrame.p.isAsync", async () => { let prevPrepareStackTrace = Error.prepareStackTrace; const prepare = mock((e, s) => { expect(s[0].isAsync()).toBeFalse(); expect(s[1].isAsync()).toBeTrue(); expect(s[2].isAsync()).toBeTrue(); expect(s[3].isAsync()).toBeTrue(); }); Error.prepareStackTrace = prepare; async function foo() { await bar(); } async function bar() { await baz(); } async function baz() { await 1; throw new Error("error from baz"); } try { await foo(); } catch (e) { e.stack; } Error.prepareStackTrace = prevPrepareStackTrace; expect(prepare).toHaveBeenCalledTimes(1); });