Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
6cacc56f76 fix(js): accept non-ErrorInstance objects in Error.prepareStackTrace
V8/Node.js allows any object as the first argument to
Error.prepareStackTrace, but Bun required a JSC ErrorInstance and threw
TypeError otherwise. This caused hangs when libraries like @babel/core
wrapped Error.prepareStackTrace and delegated to the original, because
Error.captureStackTrace on custom error classes (e.g. @xmldom/xmldom's
ParseError) would trigger the TypeError.

Closes #27708

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 09:01:02 +00:00
2 changed files with 78 additions and 2 deletions

View File

@@ -609,10 +609,14 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionDefaultErrorPrepareStackTrace, (JSGlobalObjec
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto errorObject = jsDynamicCast<JSC::ErrorInstance*>(callFrame->argument(0));
// V8 accepts any object as the first argument, not just ErrorInstance.
// Libraries like @babel/core wrap Error.prepareStackTrace and delegate to the
// original, which can be called with non-ErrorInstance objects (e.g. from
// Error.captureStackTrace on a custom error class). See #27708.
JSC::JSObject* errorObject = callFrame->argument(0).getObject();
auto callSites = jsDynamicCast<JSC::JSArray*>(callFrame->argument(1));
if (!errorObject) {
throwTypeError(lexicalGlobalObject, scope, "First argument must be an Error object"_s);
throwTypeError(lexicalGlobalObject, scope, "First argument must be an object"_s);
return {};
}
if (!callSites) {

View File

@@ -0,0 +1,72 @@
import { expect, test } from "bun:test";
// Regression test for https://github.com/oven-sh/bun/issues/27708
// Error.prepareStackTrace should accept non-ErrorInstance objects as first argument,
// matching V8/Node.js behavior. Previously, Bun threw TypeError when the first
// argument wasn't a JSC ErrorInstance, which caused hangs when libraries like
// @babel/core wrapped Error.prepareStackTrace and delegated to the original.
test("Error.prepareStackTrace accepts non-ErrorInstance objects", () => {
const orig = Error.prepareStackTrace;
try {
Error.prepareStackTrace = function (err, trace) {
return orig!(err, trace);
};
function CustomError(this: any, msg: string) {
this.message = msg;
Error.captureStackTrace(this, CustomError);
}
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;
// This should NOT throw - previously threw "First argument must be an Error object"
const err = new (CustomError as any)("test");
expect(err.stack).toBeString();
expect(err.stack).toContain("test");
} finally {
Error.prepareStackTrace = orig;
}
});
test("Error.prepareStackTrace works with class that extends Error prototype", () => {
const orig = Error.prepareStackTrace;
try {
Error.prepareStackTrace = function (err, trace) {
return orig!(err, trace);
};
// Simulate @xmldom/xmldom's ParseError pattern
function ParseError(this: any, message: string) {
this.message = message;
this.name = "ParseError";
Error.captureStackTrace(this, ParseError);
}
ParseError.prototype = Object.create(Error.prototype);
const err = new (ParseError as any)("unclosed xml tag");
expect(err.stack).toBeString();
expect(err.stack).toContain("unclosed xml tag");
} finally {
Error.prepareStackTrace = orig;
}
});
test("Error.prepareStackTrace still works normally with real Error instances", () => {
const orig = Error.prepareStackTrace;
try {
let callCount = 0;
Error.prepareStackTrace = function (err, trace) {
callCount++;
return orig!(err, trace);
};
const err = new Error("real error");
// Access stack to trigger prepareStackTrace
expect(err.stack).toBeString();
expect(err.stack).toContain("real error");
expect(callCount).toBe(1);
} finally {
Error.prepareStackTrace = orig;
}
});