Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
15 changes: 13 additions & 2 deletions src/js/node/worker_threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const EventEmitter = require("node:events");
const { SafeMap } = require("internal/primordials");
const Readable = require("internal/streams/readable");
const Writable = require("internal/streams/writable");
const { throwNotImplemented, warnNotImplementedOnce } = require("internal/shared");
const { throwNotImplemented, warnNotImplementedOnce, reportUncaughtException } = require("internal/shared");
const {
validateString,
validateObject,
Expand Down Expand Up @@ -1385,7 +1385,18 @@ class Worker extends EventEmitter {
(error as any).code = "MODULE_NOT_FOUND";
}
}
this.emit("error", error);
if (this.listenerCount("error") > 0) {
this.emit("error", error);
return;
}
// Unhandled: this becomes the parent's uncaught exception. The error came
// from another thread, so there is no throw site here worth showing —
// report the value itself rather than `throw`ing it from inside emit().
try {
this.emit("error", error); // errorMonitor listeners, ERR_UNHANDLED_ERROR wrapping
} catch (unhandled) {
reportUncaughtException(unhandled);
}
}

#onMessage(event: MessageEvent) {
Expand Down
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)
}
2 changes: 2 additions & 0 deletions src/jsc/Exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ impl Exception {
JSC__Exception__getStackTrace(self, global, stack);
}

/// The `JSC::Exception` cell itself as a JSValue (not the thrown value;
/// `JSValue::to_error` unwraps that).
pub fn value(&self) -> JSValue {
JSC__Exception__asJSValue(self)
}
Expand Down
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)
}
148 changes: 76 additions & 72 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5490,6 +5490,35 @@ impl VirtualMachine {
self.remap_stack_frames_mutex.unlock();
}

fn is_unknown_source(url: &bun_core::String) -> bool {
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:")
}

/// The frame the source preview describes — the top-most locatable
/// non-runtime frame when hiding internals, else frame 0 — and whether
/// every frame was internal/unlocatable (so frame 0 is only a fallback).
fn preview_frame(&self, frames: &[crate::ZigStackFrame]) -> (usize, bool) {
if !self.hide_bun_stackframes {
return (0, false);
}
for (i, frame) in frames.iter().enumerate() {
if frame.position.is_invalid()
|| frame.source_url.has_prefix_comptime(b"bun:")
|| frame.source_url.has_prefix_comptime(b"node:")
|| Self::is_unknown_source(&frame.source_url)
{
continue;
}
return (i, false);
}
(0, !frames.is_empty())
}

/// Fills `exception` from `error_instance`, remapping stack frames through source maps.
pub(crate) fn remap_zig_exception(
&mut self,
Expand Down Expand Up @@ -5585,9 +5614,7 @@ impl VirtualMachine {
fn is_hidden_frame(f: &crate::ZigStackFrame) -> bool {
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:")
}
let is_unknown_source = Self::is_unknown_source;

let mut frames_len = exception.stack.frames_len as usize;
// SAFETY: `frames_ptr[..frames_len]` is the caller-owned `Holder`
Expand Down Expand Up @@ -5621,13 +5648,14 @@ impl VirtualMachine {
{
continue;
}
// Note: `frames[j] = frame`. `ZigStackFrame` impls
// `Drop` so `copy_within` is unavailable; swap instead —
// the discarded tail past `j` is never read after we
// truncate `frames_len` below.
// Swap rather than copy so each frame's refs stay owned
// exactly once; the discarded frames collect past `j`.
frames_buf.swap(j, i);
j += 1;
}
for discarded in &mut frames_buf[j..frames_len] {
discarded.deinit();
}
exception.stack.frames_len = j as u8;
frames_len = j;
}
Expand All @@ -5638,27 +5666,7 @@ impl VirtualMachine {
return;
}

// Pick the top-most non-builtin frame for source preview.
let mut top: usize = 0;
let mut top_frame_is_builtin = false;
if self.hide_bun_stackframes {
for (i, frame) in frames.iter().enumerate() {
if frame.source_url.has_prefix_comptime(b"bun:")
|| frame.source_url.has_prefix_comptime(b"node:")
|| frame.source_url.is_empty()
|| frame.source_url.eql_comptime("native")
|| frame.source_url.eql_comptime("unknown")
|| frame.source_url.eql_comptime("[unknown]")
|| frame.source_url.has_prefix_comptime(b"[source:")
{
top_frame_is_builtin = true;
continue;
}
top = i;
top_frame_is_builtin = false;
break;
}
}
let (top, top_frame_is_builtin) = self.preview_frame(frames);

// Don't show source code preview for REPL frames — it would show the
// transformed IIFE wrapper code, not what the user typed.
Expand All @@ -5669,32 +5677,37 @@ impl VirtualMachine {
let top_source_url = frames[top].source_url.to_utf8();

let already_remapped = frames[top].remapped;
let maybe_lookup: Option<bun_sourcemap::mapping::Lookup> = if already_remapped {
Some(bun_sourcemap::mapping::Lookup {
mapping: bun_sourcemap::mapping::Mapping {
generated: bun_sourcemap::LineColumnOffset::default(),
original: bun_sourcemap::LineColumnOffset {
lines: bun_sourcemap::Ordinal::from_zero_based(
frames[top].position.line.zero_based().max(0),
),
columns: bun_sourcemap::Ordinal::from_zero_based(
frames[top].position.column.zero_based().max(0),
),
let maybe_lookup: Option<bun_sourcemap::mapping::Lookup> =
if already_remapped && !frames[top].position.line.is_valid() {
// `at <file>` with no line (e.g. parsed from a `.stack`): nothing
// to look up or preview.
None
} else if already_remapped {
Some(bun_sourcemap::mapping::Lookup {
mapping: bun_sourcemap::mapping::Mapping {
generated: bun_sourcemap::LineColumnOffset::default(),
original: bun_sourcemap::LineColumnOffset {
lines: bun_sourcemap::Ordinal::from_zero_based(
frames[top].position.line.zero_based().max(0),
),
columns: bun_sourcemap::Ordinal::from_zero_based(
frames[top].position.column.zero_based().max(0),
),
},
source_index: 0,
name_index: -1,
},
source_index: 0,
name_index: -1,
},
source_map: None,
prefetched_source_code: None,
})
} else {
self.resolve_source_mapping(
top_source_url.slice(),
frames[top].position.line,
frames[top].position.column,
bun_sourcemap::SourceContentHandling::SourceContents,
)
};
source_map: None,
prefetched_source_code: None,
})
} else {
self.resolve_source_mapping(
top_source_url.slice(),
frames[top].position.line,
frames[top].position.column,
bun_sourcemap::SourceContentHandling::SourceContents,
)
};

if let Some(lookup) = maybe_lookup {
// The source-map Arc drops on scope exit.
Expand Down Expand Up @@ -5767,12 +5780,14 @@ impl VirtualMachine {
};

if enable_source_code_preview.get() && code.slice().is_empty() {
exception.collect_source_lines(error_instance, global);
exception.collect_source_lines(top);
}

// Direct copy; both sides are `bun_core::Ordinal`.
frames[top].position.line = mapping.original.lines;
frames[top].position.column = mapping.original.columns;
if !already_remapped {
// Direct copy; both sides are `bun_core::Ordinal`.
frames[top].position.line = mapping.original.lines;
frames[top].position.column = mapping.original.columns;
}
exception.remapped = true;
frames[top].remapped = true;

Expand Down Expand Up @@ -5811,14 +5826,14 @@ impl VirtualMachine {
*source_code_slice = Some(code);
}
} else if enable_source_code_preview.get() {
exception.collect_source_lines(error_instance, global);
exception.collect_source_lines(top);
}

drop(top_source_url);

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 Expand Up @@ -6163,19 +6178,8 @@ impl VirtualMachine {
}

let frames = exception.stack.frames();
let mut top_frame: Option<&crate::ZigStackFrame> = frames.first();
if self.hide_bun_stackframes {
for frame in frames {
if frame.position.is_invalid()
|| frame.source_url.has_prefix_comptime(b"bun:")
|| frame.source_url.has_prefix_comptime(b"node:")
{
continue;
}
top_frame = Some(frame);
break;
}
}
let top_frame: Option<&crate::ZigStackFrame> =
frames.get(self.preview_frame(frames).0);

let trimmed = source.trimmed_text();

Expand Down
16 changes: 8 additions & 8 deletions src/jsc/ZigException.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ use bun_url::URL as ZigURL;

use crate::module_loader::ModuleLoader;
use crate::virtual_machine::VirtualMachine;
use crate::{JSErrorCode, JSGlobalObject, JSRuntimeType, JSValue, ZigStackFrame, ZigStackTrace};
use crate::{JSErrorCode, JSRuntimeType, ZigStackFrame, ZigStackTrace};

// SAFETY (safe fn): `JSValue` is a by-value scalar; `JSGlobalObject` is an
// opaque `UnsafeCell`-backed handle (`&` is ABI-identical to non-null `*mut`);
// `ZigException` is a `#[repr(C)]` out-param the C++ side fills in-place.
// SAFETY (safe fn): `ZigException` is a `#[repr(C)]` out-param the C++ side
// fills in-place.
unsafe extern "C" {
pub(crate) safe fn ZigException__collectSourceLines(
js_value: JSValue,
global: &JSGlobalObject,
exception: &mut ZigException,
top_frame: u8,
);
}

Expand Down Expand Up @@ -50,8 +48,10 @@ pub struct ZigException {
}

impl ZigException {
pub(crate) fn collect_source_lines(&mut self, value: JSValue, global: &JSGlobalObject) {
ZigException__collectSourceLines(value, global, self);
/// Slice frame `top_frame`'s source lines out of the SourceProvider it was
/// populated from (for sources the printer can't read from disk).
pub(crate) fn collect_source_lines(&mut self, top_frame: usize) {
ZigException__collectSourceLines(self, u8::try_from(top_frame).unwrap_or(u8::MAX));
}

// Kept as explicit `deinit` (not `Drop`) — this is a #[repr(C)] FFI
Expand Down
Loading
Loading