Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
61eb3e4
Restore reject_bad_negations initializer lost in branch merge
cirospaciari Jul 25, 2026
be6148f
cli: restore -e/-p/--check/--inspect-port handling and fs arg validat…
cirospaciari Jul 25, 2026
9630ecb
print uncaught errors Node-style: real error name, single-quoted stri…
cirospaciari Jul 25, 2026
fb1c113
module resolution errors: Node's name/toString/stack surface
cirospaciari Jul 25, 2026
4319cb3
vendor permission tests unlocked by the Node-style uncaught printer
cirospaciari Jul 25, 2026
ec025af
Merge remote-tracking branch 'origin/claude/node-v26-fix-tls' into cl…
robobun Aug 3, 2026
bc76303
trim comments to <=3 lines, cite spec/node source
robobun Aug 3, 2026
d00ec2c
Merge remote-tracking branch 'origin/claude/node-v26-fix-tls' into cl…
robobun Aug 3, 2026
8fc22b0
fix build: restore BunHeapProfiler.h + repair merge fallout in bun_js…
robobun Aug 4, 2026
3669a9a
Merge remote-tracking branch 'origin/claude/node-v26-fix-tls' into cl…
robobun Aug 4, 2026
9756e80
Merge remote-tracking branch 'origin/claude/node-v26-fix-tls' into cl…
robobun Aug 4, 2026
057a646
Merge remote-tracking branch 'origin/claude/node-v26-fix-tls' into cl…
robobun Aug 4, 2026
58d7581
Merge remote-tracking branch 'origin/claude/node-v26-fix-tls' into cl…
robobun Aug 4, 2026
9a8f8ce
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 4, 2026
b9aabef
Merge branch 'claude/node-v26-fix-tls' into claude/node-uncaught-prin…
robobun Aug 21, 2026
7b90209
Merge branch 'claude/node-v26-fix-tls' into claude/node-uncaught-prin…
robobun Aug 21, 2026
a82e29b
Merge branch 'claude/node-v26-fix-tls' into claude/node-uncaught-prin…
robobun Aug 21, 2026
adc7c9f
Merge branch 'claude/node-v26-fix-tls' into claude/node-uncaught-prin…
robobun Aug 21, 2026
05e8248
test: restore the upstream Error: expectations in two vendored tests …
robobun Aug 21, 2026
0398781
test: drop test-internal-modules.js, the test harness exposes interna…
robobun Aug 21, 2026
38783fd
test: update assertions on uncaught-error output and resolve-error na…
robobun Aug 21, 2026
c51429f
Merge branch 'claude/node-v26-fix-tls' into claude/node-uncaught-prin…
robobun Aug 22, 2026
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/js/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ var _extend = function (origin, add) {

function callbackifyOnRejected(reason, cb) {
if (!reason) {
var newReason = new Error("Promise was rejected with a falsy value");
var newReason = new Error("Promise was rejected with falsy value");
newReason.reason = reason;
newReason.code = "ERR_FALSY_VALUE_REJECTION";
reason = newReason;
Expand Down
37 changes: 36 additions & 1 deletion src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1748,6 +1748,10 @@ pub mod formatter {
/// printed as a string. Set true in the error printer so that
/// `ShellError` prints a more readable message.
pub(crate) format_buffer_as_text: bool,
/// Node-style uncaught report: verbatim `err.name` (no lowercase `error:`
/// alias), single-quoted strings (`code: 'ENOENT'`). Uncaught path only.
/// https://github.com/nodejs/node/blob/main/lib/internal/util/inspect.js
pub node_uncaught_style: bool,
}

impl<'a> Formatter<'a> {
Expand Down Expand Up @@ -1778,6 +1782,7 @@ pub mod formatter {
can_throw_stack_overflow: false,
error_display_level: ErrorDisplayLevel::Full,
format_buffer_as_text: false,
node_uncaught_style: false,
}
}

Expand Down Expand Up @@ -1813,6 +1818,7 @@ pub mod formatter {
can_throw_stack_overflow: self.can_throw_stack_overflow,
error_display_level: self.error_display_level,
format_buffer_as_text: self.format_buffer_as_text,
node_uncaught_style: self.node_uncaught_style,
}
}

Expand Down Expand Up @@ -3679,7 +3685,36 @@ pub mod formatter {

if self.quote_strings && js_type != jsc::JSType::RegExpObject {
if str.is_empty() {
writer.write_all(b"\"\"");
writer.write_all(if self.node_uncaught_style {
b"''"
} else {
b"\"\""
});
if writer.failed {
self.failed = true;
}
return Ok(());
}

// Node quotes inspect strings with single quotes. Only simple
// ASCII strings take this path; anything needing escapes falls
// through to the JSON (double-quote) writer.
if self.node_uncaught_style
&& !str.is_utf16()
&& str
.latin1()
.iter()
.all(|&b| (0x20..0x7f).contains(&b) && b != b'\'' && b != b'\\')
{
if C {
writer.write_all(pfmt!("<r><green>", true).as_bytes());
}
writer.write_all(b"'");
writer.write_all(str.latin1());
writer.write_all(b"'");
if C {
writer.write_all(pfmt!("<r>", true).as_bytes());
}
if writer.failed {
self.failed = true;
}
Expand Down
70 changes: 66 additions & 4 deletions src/jsc/ResolveMessage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ fn is_bare_esm_specifier(s: &[u8]) -> bool {
[d, b':', b, ..] if d.is_ascii_alphabetic() && is_sep(*b) => return false,
_ => {}
}
// Leading URL scheme (`[A-Za-z][A-Za-z0-9+.-]*:`, RFC 3986 §3.1) is not a
// bare package — Node reports scheme errors, not "Cannot find package 'file:'".
// https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/resolve.js
if s[0].is_ascii_alphabetic() {
for (i, &b) in s.iter().enumerate() {
match b {
b':' if i > 0 => return false,
b if b.is_ascii_alphanumeric() || b == b'+' || b == b'.' || b == b'-' => {}
_ => break,
}
}
}
true
}

Expand Down Expand Up @@ -243,7 +255,14 @@ impl ResolveMessage {
// cases.
let node_message = self.node_message();
let message: &[u8] = node_message.as_deref().unwrap_or(&self.msg.data.text);
if write!(&mut text, "ResolveMessage: {}", bstr::BStr::new(message)).is_err() {
if write!(
&mut text,
"{}: {}",
bstr::BStr::new(self.node_display_name().unwrap_or(b"ResolveMessage")),
bstr::BStr::new(message)
)
.is_err()
{
return global.throw_out_of_memory_value();
}
let mut str = ZigString::init(&text);
Expand Down Expand Up @@ -300,7 +319,7 @@ impl ResolveMessage {
object.put(
global,
b"name",
bun_core::String::static_str(b"ResolveMessage").to_js(global)?,
bun_core::String::static_str(this.js_name()).to_js(global)?,
);
object.put(global, b"position", Self::get_position(this, global)?);
object.put(global, b"message", Self::get_message(this, global)?);
Expand Down Expand Up @@ -372,9 +391,51 @@ impl ResolveMessage {
Some((resolve.import_kind, specifier, referrer))
}

/// Whether this is Node's ERR_UNKNOWN_BUILTIN_MODULE (`require('node:x')`
/// / `import('node:x')` for a builtin that doesn't exist).
fn is_unknown_builtin(&self) -> bool {
let bun_ast::Metadata::Resolve(resolve) = &self.msg.metadata else {
return false;
};
matches!(
resolve.import_kind,
ImportKind::Require | ImportKind::Stmt | ImportKind::Dynamic
) && self.msg.data.text.starts_with(b"No such built-in module:")
}

/// `err.name` — Node throws module-resolution failures as plain `Error`.
pub(crate) fn js_name(&self) -> &'static [u8] {
if self.node_display_name().is_some() {
b"Error"
} else {
b"ResolveMessage"
}
}

/// `toString()`/`.stack` name, or `None` to keep Bun's ResolveMessage render.
/// Node's `E()` errors bracket the code (`Error [ERR_MODULE_NOT_FOUND]`); CJS
/// is plain `Error`. https://github.com/nodejs/node/blob/main/lib/internal/errors.js
pub(crate) fn node_display_name(&self) -> Option<&'static [u8]> {
if self.is_unknown_builtin() {
return Some(b"Error [ERR_UNKNOWN_BUILTIN_MODULE]");
}
match self.node_error_shape() {
Some((ImportKind::Require | ImportKind::RequireResolve, ..)) => Some(b"Error"),
Some((ImportKind::Stmt | ImportKind::Dynamic, ..)) => {
Some(b"Error [ERR_MODULE_NOT_FOUND]")
}
_ => None,
}
}

#[crate::host_fn(getter)]
pub fn get_name(this: &Self, global: &JSGlobalObject) -> JsResult<JSValue> {
Ok(ZigString::init(this.js_name()).to_js(global))
}

/// Node's message for a module-not-found error, or `None` when the
/// original text should be kept.
fn node_message(&self) -> Option<Vec<u8>> {
pub(crate) fn node_message(&self) -> Option<Vec<u8>> {
use bstr::BStr;
let (kind, specifier, referrer) = self.node_error_shape()?;
let mut out = Vec::new();
Expand Down Expand Up @@ -440,7 +501,8 @@ impl ResolveMessage {
#[crate::host_fn(getter)]
pub fn get_stack(this: &Self, global: &JSGlobalObject) -> JsResult<JSValue> {
let mut out = Vec::new();
out.extend_from_slice(b"ResolveMessage: ");
out.extend_from_slice(this.node_display_name().unwrap_or(b"ResolveMessage"));
out.extend_from_slice(b": ");
match this.node_message() {
Some(text) => out.extend_from_slice(&text),
None => out.extend_from_slice(&this.msg.data.text),
Expand Down
139 changes: 98 additions & 41 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1597,7 +1597,10 @@ impl VirtualMachine {
(self.on_unhandled_rejection)(self, global_object, err);
return false;
}
self.run_error_handler(err, None);
// `err` may be the JSC::Exception wrapper (handler throw re-reported
// via `report_active_exception_as_unhandled`); unwrap so the printer
// sees the Error's own stack/properties, not the rethrow-site capture.
self.run_error_handler(err.to_error().unwrap_or(err), None);
// SAFETY: `global_object` is the live VM global; `process_exit` is
// `bun_runtime::node::process::exit` (main-thread `noreturn`).
unsafe { (hooks.process_exit)(global_object.as_ptr(), 7) };
Expand Down Expand Up @@ -4968,6 +4971,10 @@ impl VirtualMachine {
allow_side_effects: bool,
) {
let mut formatter = crate::console_object::Formatter::new(self.global());
// `bun test` keeps Bun's classic failure rendering; everything else
// (uncaught exceptions, unhandled rejections, server error handlers)
// prints Node's shape.
formatter.node_uncaught_style = !isBunTest.load(core::sync::atomic::Ordering::Relaxed);
let colors = bun_core::Output::enable_ansi_colors_stderr();
self.print_errorlike_object(
exception.value(),
Expand Down Expand Up @@ -5486,7 +5493,35 @@ impl VirtualMachine {
if self.had_errors {
let _ = writer.write_all(b"\n");
}
write_msg!(resolve_error.msg, writer, allow_ansi_color);
// Uncaught module-resolution errors print Node's shape
// (`Error: Cannot find module 'x'\nRequire stack:\n- ...`),
// not the transpiler-log rendering.
if let Some(display_name) = resolve_error
.node_display_name()
.filter(|_| formatter.node_uncaught_style)
{
let node_message = resolve_error.node_message();
let message: &[u8] = node_message
.as_deref()
.unwrap_or(&resolve_error.msg.data.text);
let _ = if allow_ansi_color {
write!(
writer,
bun_core::pretty_fmt!("<red>{}<r><d>:<r> <b>{}<r>", true),
bstr::BStr::new(display_name),
bstr::BStr::new(message)
)
} else {
write!(
writer,
"{}: {}",
bstr::BStr::new(display_name),
bstr::BStr::new(message)
)
};
} else {
write_msg!(resolve_error.msg, writer, allow_ansi_color);
}
resolve_error.logged.set(true);
let _ = writer.write_all(b"\n");
}
Expand Down Expand Up @@ -6391,6 +6426,7 @@ impl VirtualMachine {
message,
!exception.browser_url.is_empty(),
code,
formatter.node_uncaught_style,
writer,
allow_ansi_color,
formatter.error_display_level,
Expand Down Expand Up @@ -6441,6 +6477,7 @@ impl VirtualMachine {
message,
!exception.browser_url.is_empty(),
code,
formatter.node_uncaught_style,
writer,
allow_ansi_color,
formatter.error_display_level,
Expand All @@ -6456,6 +6493,7 @@ impl VirtualMachine {
message,
!exception.browser_url.is_empty(),
code,
formatter.node_uncaught_style,
writer,
allow_ansi_color,
formatter.error_display_level,
Expand Down Expand Up @@ -6593,11 +6631,24 @@ impl VirtualMachine {
let pad_left = longest_name.saturating_sub(b"code".len());
is_first_property = false;
splat_space(writer, pad_left as u64)?;
pretty_write!(
writer,
" code<r><d>:<r> <green>{}<r>\n",
bun_core::fmt::quote(code_str)
)?;
if formatter.node_uncaught_style
&& code_str
.iter()
.all(|&b| (0x20..0x7f).contains(&b) && b != b'\'' && b != b'\\')
{
// Node single-quotes inspect strings: `code: 'ENOENT'`.
pretty_write!(
writer,
" code<r><d>:<r> <green>'{}'<r>\n",
bstr::BStr::new(code_str)
)?;
} else {
pretty_write!(
writer,
" code<r><d>:<r> <green>{}<r>\n",
bun_core::fmt::quote(code_str)
)?;
}
}

if !is_first_property {
Expand Down Expand Up @@ -6675,11 +6726,13 @@ impl VirtualMachine {
Ok(())
}

#[allow(clippy::too_many_arguments)]
fn print_error_name_and_message(
name: bun_core::String,
message: bun_core::String,
is_browser_error: bool,
optional_code: Option<&[u8]>,
node_uncaught_style: bool,
writer: &mut bun_core::io::Writer,
allow_ansi_color: bool,
error_display_level: crate::console_object::ErrorDisplayLevel,
Expand All @@ -6698,43 +6751,47 @@ impl VirtualMachine {
writer.write_all(bun_core::pretty_fmt!("<red>frontend<r> ", true).as_bytes())?;
}
if !name.is_empty() && !message.is_empty() {
let (display_name, display_message) = if name.eql_comptime(b"Error") {
'brk: {
if let Some(code) = optional_code {
if bun_core::is_all_ascii(code) {
let has_prefix = if message.is_utf16() {
let msg_chars = message.utf16();
msg_chars.len() > code.len() + 2 + 1
&& code
.iter()
.zip(msg_chars.iter())
.all(|(&a, &b)| u16::from(a) == b)
&& msg_chars[code.len()] == u16::from(b':')
&& msg_chars[code.len() + 1] == u16::from(b' ')
} else {
let msg_chars = message.latin1();
msg_chars.len() > code.len() + 2 + 1
&& bun_core::strings::eql_long(
&msg_chars[..code.len()],
code,
false,
)
&& msg_chars[code.len()] == b':'
&& msg_chars[code.len() + 1] == b' '
};
if has_prefix {
break 'brk (
bun_core::String::init(code),
message.substring(code.len() + 2),
);
// Node prints the error's own name verbatim (`Error: boom`); Bun's
// classic rendering lowercases plain `Error` to `error:` and
// promotes a `CODE: `-prefixed message's code to the name slot.
let (display_name, display_message) =
if name.eql_comptime(b"Error") && !node_uncaught_style {
'brk: {
if let Some(code) = optional_code {
if bun_core::is_all_ascii(code) {
let has_prefix = if message.is_utf16() {
let msg_chars = message.utf16();
msg_chars.len() > code.len() + 2 + 1
&& code
.iter()
.zip(msg_chars.iter())
.all(|(&a, &b)| u16::from(a) == b)
&& msg_chars[code.len()] == u16::from(b':')
&& msg_chars[code.len() + 1] == u16::from(b' ')
} else {
let msg_chars = message.latin1();
msg_chars.len() > code.len() + 2 + 1
&& bun_core::strings::eql_long(
&msg_chars[..code.len()],
code,
false,
)
&& msg_chars[code.len()] == b':'
&& msg_chars[code.len() + 1] == b' '
};
if has_prefix {
break 'brk (
bun_core::String::init(code),
message.substring(code.len() + 2),
);
}
}
}
(bun_core::String::empty(), message)
}
(bun_core::String::empty(), message)
}
} else {
(name, message)
};
} else {
(name, message)
};
pretty_write!(
"{}<b>{}<r>\n",
error_display_level.formatter(display_name, allow_ansi_color, Colon::IncludeColon),
Expand Down
Loading
Loading