Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
28 changes: 28 additions & 0 deletions src/jsc/BuildMessage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,25 @@ impl BuildMessage {
Ok(build_error.to_js(global))
}

/// The `SyntaxError` this diagnostic becomes when it has to leave the realm
/// it was created in (structured clone / worker error reporting): a
/// `BuildMessage` wraps parser state that can't cross, but its message and
/// file/line/column can.
pub fn to_error_instance(&self, global: &JSGlobalObject) -> JSValue {
let (file, line, column): (&[u8], i32, i32) = match &self.msg.data.location {
Some(loc) => (&loc.file, loc.line, loc.column),
None => (b"", 0, 0),
};
crate::bun_string_jsc::error_instance_from_location(
global,
crate::JSErrorCode::SyntaxError,
&self.msg.data.text,
file,
line,
column,
)
}

#[crate::host_fn(method)]
pub fn to_string(&self, global: &JSGlobalObject, _frame: &CallFrame) -> JsResult<JSValue> {
Ok(self.to_string_fn(global))
Expand Down Expand Up @@ -198,3 +217,12 @@ impl BuildMessage {
Ok(ZigString::init(self.msg.kind.string()).to_js(global))
}
}

// SerializedScriptValue.cpp
#[unsafe(no_mangle)]
extern "C" fn BuildMessage__toErrorInstance(
this: &BuildMessage,
global: &JSGlobalObject,
) -> JSValue {
this.to_error_instance(global)
}
7 changes: 7 additions & 0 deletions src/jsc/Exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ unsafe extern "C" {
stack: &mut ZigStackTrace,
);
safe fn JSC__Exception__asJSValue(this: &Exception) -> JSValue;
safe fn JSC__Exception__thrownValue(this: &Exception) -> JSValue;
}

impl Exception {
pub(crate) fn get_stack_trace(&self, global: &JSGlobalObject, stack: &mut ZigStackTrace) {
JSC__Exception__getStackTrace(self, global, stack);
}

/// The `JSC::Exception` cell itself, as a JSValue.
pub fn value(&self) -> JSValue {
JSC__Exception__asJSValue(self)
}

/// The value that was thrown (what the `JSC::Exception` wraps).
pub fn thrown_value(&self) -> JSValue {
JSC__Exception__thrownValue(self)
}
}
8 changes: 8 additions & 0 deletions src/jsc/JSErrorCode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ pub struct JSErrorCode(pub u8);

#[allow(non_upper_case_globals)]
impl JSErrorCode {
// JSC::ErrorType (JavaScriptCore/ErrorType.h)
pub const Error: Self = Self(0);
pub const EvalError: Self = Self(1);
pub const RangeError: Self = Self(2);
pub const ReferenceError: Self = Self(3);
pub const SyntaxError: Self = Self(4);
pub const TypeError: Self = Self(5);
pub const URIError: Self = Self(6);
pub const AggregateError: Self = Self(7);

// StackOverflow & OutOfMemoryError is not an ErrorType in "JavaScriptCore/ErrorType.h" within JSC, so the number here is just totally made up
}
Expand Down
28 changes: 28 additions & 0 deletions src/jsc/ResolveMessage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,8 +494,36 @@ impl ResolveMessage {
})
}

/// See `BuildMessage::to_error_instance`.
pub fn to_error_instance(&self, global: &JSGlobalObject) -> JSValue {
let node_message = self.node_message();
// Runtime resolution failures know the importing file (`referrer`) but
// carry no `location`; either way the importer is where this points.
let (file, line, column): (&[u8], i32, i32) = match &self.msg.data.location {
Some(loc) => (&loc.file, loc.line, loc.column),
None => (self.referrer.as_deref().unwrap_or(b""), 0, 0),
};
crate::bun_string_jsc::error_instance_from_location(
global,
crate::JSErrorCode::Error,
node_message.as_deref().unwrap_or(&self.msg.data.text),
file,
line,
column,
)
}

pub fn finalize(self: Box<Self>) {
// Dropping the Box drops `msg` and the owned `referrer` buffer.
drop(self);
}
}

// SerializedScriptValue.cpp
#[unsafe(no_mangle)]
extern "C" fn ResolveMessage__toErrorInstance(
this: &ResolveMessage,
global: &JSGlobalObject,
) -> JSValue {
this.to_error_instance(global)
}
19 changes: 16 additions & 3 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5235,7 +5235,15 @@ impl VirtualMachine {
};
}

if value.js_type() == jsc::JSType::DOMWrapper {
// A thrown (rather than rejected) diagnostic arrives wrapped in its
// JSC::Exception; look through it so it still prints as a diagnostic.
let thrown = match value.as_exception(self.jsc_vm) {
// SAFETY: `as_exception` proved `value` is a live `JSC::Exception` cell.
Some(exception) => unsafe { &*exception }.thrown_value(),
None => value,
};
if thrown.js_type() == jsc::JSType::DOMWrapper {
let value = thrown;
// `as_class_ref` is the audited `as_::<T>() → &T` backref-deref;
// R-2: shared borrow — `logged` is `Cell<bool>`.
if let Some(build_error) = value.as_class_ref::<crate::BuildMessage>() {
Expand Down Expand Up @@ -5543,7 +5551,12 @@ impl VirtualMachine {
f.source_url.eql_comptime("bun:wrap") || f.function_name.eql_comptime("::bunternal::")
}
fn is_unknown_source(url: &bun_core::String) -> bool {
url.is_empty() || url.eql_comptime("[unknown]") || url.has_prefix_comptime(b"[source:")
url.is_empty()
|| url.eql_comptime("[unknown]")
// FormatStackTraceForJS spells them this way in `.stack` strings.
|| url.eql_comptime("unknown")
|| url.eql_comptime("native")
|| url.has_prefix_comptime(b"[source:")
}

let mut frames_len = exception.stack.frames_len as usize;
Expand Down Expand Up @@ -5775,7 +5788,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
2 changes: 2 additions & 0 deletions src/jsc/ZigException.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub struct ZigException {
pub exception: *mut c_void,

pub remapped: bool,
pub frames_are_throw_site: bool,

pub fd: i32,

Expand Down Expand Up @@ -178,6 +179,7 @@ impl Holder {
system_code: String::EMPTY,
path: String::EMPTY,
remapped: false,
frames_are_throw_site: false,
fd: -1,
browser_url: String::EMPTY,
});
Expand Down
76 changes: 45 additions & 31 deletions src/jsc/bindings/ZigException.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ class V8StackTraceIterator {
public:
StringView functionName {};
StringView sourceURL {};
WTF::OrdinalNumber lineNumber = WTF::OrdinalNumber::fromZeroBasedInt(0);
WTF::OrdinalNumber columnNumber = WTF::OrdinalNumber::fromZeroBasedInt(0);
WTF::OrdinalNumber lineNumber = WTF::OrdinalNumber::beforeFirst();
WTF::OrdinalNumber columnNumber = WTF::OrdinalNumber::beforeFirst();

bool isConstructor = false;
bool isGlobalCode = false;
Expand Down Expand Up @@ -296,22 +296,21 @@ class V8StackTraceIterator {
if (openingParentheses > closingParentheses)
openingParentheses = WTF::notFound;

StringView functionName;
StringView lineInner;
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
if (line == "unknown"_s) {
frame.sourceURL = line;
frame.functionName = StringView();
return true;
// `at <url>:<line>:<column>` — anonymous and top-level frames carry
// no name and no parentheses (V8 and Bun both print them this way).
lineInner = line;
if (lineInner.startsWith("async "_s)) {
frame.isAsync = true;
lineInner = lineInner.substring(6);
}

// For any other frame without parentheses, terminate parsing as before
offset = stack.length();
return false;
} 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 +382,6 @@ class V8StackTraceIterator {
}
done_block:

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

if (functionName == "global code"_s) {
functionName = StringView();
frame.isGlobalCode = true;
Expand All @@ -400,10 +397,6 @@ class V8StackTraceIterator {
functionName = functionName.substring(4);
}

if (functionName == "<anonymous>"_s) {
functionName = StringView();
}

frame.functionName = functionName;

return true;
Expand Down Expand Up @@ -479,13 +472,15 @@ static void fromErrorInstance(ZigException& except, JSC::JSGlobalObject* global,
auto& vm = JSC::getVM(global);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);

// An Error is described by where it was created: its own captured frames,
// or — once those have been materialized into a `.stack` string, or were
// only ever supplied as one (structured clone, Error.captureStackTrace) —
// that string. `stackTrace`, the wrapping JSC::Exception's throw site, is
// the fallback for errors carrying neither, so a rethrown or cross-thread
// error points at its origin rather than at `throw err`.
bool getFromSourceURL = false;
if (stackTrace != nullptr && stackTrace->size() > 0) {
populateStackTrace(vm, *stackTrace, except.stack, global, flags);

} else if (err->stackTrace() != nullptr && err->stackTrace()->size() > 0) {
if (err->stackTrace() != nullptr && err->stackTrace()->size() > 0) {
populateStackTrace(vm, *err->stackTrace(), except.stack, global, flags, FinalizerSafety::MustNotTriggerGC);

} else {
getFromSourceURL = true;
}
Expand Down Expand Up @@ -602,10 +597,13 @@ static void fromErrorInstance(ZigException& except, JSC::JSGlobalObject* global,

iterator.forEachFrame([&](const V8StackTraceIterator::StackFrame& frame, bool& stop) -> void {
ASSERT(except.stack.frames_len < frame_count);
// populateStackTrace skips native frames; these are how they print.
if (frame.lineNumber.zeroBasedInt() < 0 && (frame.sourceURL.isEmpty() || frame.sourceURL == "unknown"_s || frame.sourceURL == "native"_s))
return;
auto& current = except.stack.frames_ptr[except.stack.frames_len];
current = {};

String functionName = frame.functionName.toString();
String functionName = frame.functionName == "<anonymous>"_s ? String() : frame.functionName.toString();
String sourceURL = frame.sourceURL.toString();
current.function_name = Bun::toStringRef(functionName);
current.source_url = Bun::toStringRef(sourceURL);
Expand All @@ -619,6 +617,8 @@ static void fromErrorInstance(ZigException& except, JSC::JSGlobalObject* global,
current.code_type = ZigStackFrameCodeConstructor;
} else if (frame.isGlobalCode) {
current.code_type = ZigStackFrameCodeGlobal;
} else if (!frame.functionName.isEmpty()) {
current.code_type = ZigStackFrameCodeFunction;
}

except.stack.frames_len += 1;
Expand All @@ -635,6 +635,16 @@ static void fromErrorInstance(ZigException& except, JSC::JSGlobalObject* global,
}
}

bool framesAreThrowSite = false;
if (except.stack.frames_len == 0 && stackTrace != nullptr && stackTrace->size() > 0) {
populateStackTrace(vm, *stackTrace, except.stack, global, flags);
if (except.stack.frames_len > 0) {
getFromSourceURL = false;
framesAreThrowSite = true;
}
}
except.frames_are_throw_site = framesAreThrowSite;

if (except.stack.frames_len == 0 && getFromSourceURL) {
JSC::JSValue sourceURL = getNonObservable(vm, global, obj, vm.propertyNames->sourceURL);
if (!scope.clearExceptionExceptTermination()) [[unlikely]]
Expand Down Expand Up @@ -877,12 +887,16 @@ extern "C" void ZigException__collectSourceLines(JSC::EncodedJSValue jsException
auto* jscException = uncheckedDowncast<JSC::Exception>(value);
JSValue unwrapped = jscException->value();

if (jscException->stack().size() > 0) {
populateStackTrace(global->vm(), jscException->stack(), exception->stack, global, PopulateStackTraceFlags::OnlySourceLines);
// Revisit whichever frames JSC__JSValue__toZigException populated: the
// error's own unless it had none and fell back to the throw site.
if (dynamicDowncast<JSC::ErrorInstance>(unwrapped) && !exception->frames_are_throw_site) {
value = unwrapped;
} else {
if (jscException->stack().size() > 0) {
populateStackTrace(global->vm(), jscException->stack(), exception->stack, global, PopulateStackTraceFlags::OnlySourceLines);
}
return;
}

exceptionFromString(*exception, unwrapped, global);
return;
}

if (JSC::ErrorInstance* error = dynamicDowncast<JSC::ErrorInstance>(value)) {
Expand Down
16 changes: 16 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4883,6 +4883,11 @@ extern "C" JSC::EncodedJSValue JSC__Exception__asJSValue(JSC::Exception* excepti
return JSC::JSValue::encode(exception);
}

extern "C" JSC::EncodedJSValue JSC__Exception__thrownValue(JSC::Exception* exception)
{
return JSC::JSValue::encode(exception->value());
}

void JSC__VM__releaseWeakRefs(JSC::VM* arg0)
{
arg0->finalizeSynchronousJSExecution();
Expand Down Expand Up @@ -6348,6 +6353,17 @@ extern "C" EncodedJSValue JSC__createRangeError(JSC::JSGlobalObject* globalObjec
return JSValue::encode(JSC::createRangeError(globalObject, str->toWTFString(BunString::ZeroCopy)));
}

// An ErrorInstance whose location is supplied rather than captured from the
// current JS stack — the same constructor structured-clone deserialization
// uses. `line`/`column`/`sourceURL`/`stack` materialize lazily as the usual
// non-enumerable own properties.
extern "C" EncodedJSValue JSC__createErrorWithLocation(JSC::JSGlobalObject* globalObject, uint8_t errorType, const BunString* message, const BunString* sourceURL, int32_t line, int32_t column, const BunString* stack)
{
return JSValue::encode(JSC::ErrorInstance::create(globalObject, message->toWTFString(), static_cast<JSC::ErrorType>(errorType),
{ line < 0 ? 0u : static_cast<unsigned>(line), column < 0 ? 0u : static_cast<unsigned>(column) },
sourceURL->toWTFString(), stack->toWTFString()));
}

extern "C" EncodedJSValue ExpectMatcherUtils__getSingleton(JSC::JSGlobalObject* globalObject_)
{
Zig::GlobalObject* globalObject = static_cast<Zig::GlobalObject*>(globalObject_);
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ typedef struct ZigException {
ZigStackTrace stack;
void* exception;
bool remapped;
// `stack` holds the wrapping JSC::Exception's throw site (the error had no
// frames or `.stack` of its own), so source lines come from there too.
bool frames_are_throw_site;
int fd;
} ZigException;

Expand Down
22 changes: 20 additions & 2 deletions src/jsc/bindings/webcore/SerializedScriptValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,24 @@ enum SerializationTag {
// pointer is gone.
extern "C" SYSV_ABI void BlockList__onStructuredCloneDestroy(void*);

extern "C" JSC::EncodedJSValue BuildMessage__toErrorInstance(void*, JSC::JSGlobalObject*);
extern "C" JSC::EncodedJSValue ResolveMessage__toErrorInstance(void*, JSC::JSGlobalObject*);

// The ErrorInstance to write for `object`, if it is an error. Bun's parse and
// resolve diagnostics have Error.prototype in their chain but wrap VM-local
// parser state, so they serialize as the plain SyntaxError / Error carrying
// their message and location.
static JSC::ErrorInstance* toSerializableErrorInstance(JSC::JSGlobalObject* globalObject, JSC::JSObject* object)
{
if (auto* errorInstance = dynamicDowncast<JSC::ErrorInstance>(object))
return errorInstance;
if (auto* buildMessage = dynamicDowncast<JSBuildMessage>(object))
return uncheckedDowncast<JSC::ErrorInstance>(JSC::JSValue::decode(BuildMessage__toErrorInstance(buildMessage->wrapped(), globalObject)));
if (auto* resolveMessage = dynamicDowncast<JSResolveMessage>(object))
return uncheckedDowncast<JSC::ErrorInstance>(JSC::JSValue::decode(ResolveMessage__toErrorInstance(resolveMessage->wrapped(), globalObject)));
return nullptr;
}

enum ArrayBufferViewSubtag {
DataViewTag = 0,
Int8ArrayTag = 1,
Expand Down Expand Up @@ -1183,8 +1201,8 @@ class CloneSerializer : public CloneBase {
write(String::fromLatin1(JSC::Yarr::flagsString(regExp->regExp()->flags()).data()));
return true;
}
if (auto* errorInstance = dynamicDowncast<ErrorInstance>(obj)) {
if (!startObjectInternal(errorInstance)) // handle duplicates
if (auto* errorInstance = toSerializableErrorInstance(m_lexicalGlobalObject, obj)) {
if (!startObjectInternal(obj)) // handle duplicates
return true;
auto& vm = m_lexicalGlobalObject->vm();
auto errorTypeValue = errorInstance->get(m_lexicalGlobalObject, vm.propertyNames->name);
Expand Down
Loading