diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index a9270cf8866..66f28a70123 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -4881,8 +4881,13 @@ pub mod formatter { } let event_type_value: JSValue = 'brk: { - let Some(value_) = value.get(self.global_this, "type")? else { - break 'brk JSValue::UNDEFINED; + let value_ = match value.get(self.global_this, "type") { + Ok(Some(v)) => v, + Ok(None) => break 'brk JSValue::UNDEFINED, + Err(_) => { + self.global_this.clear_exception(); + break 'brk JSValue::UNDEFINED; + } }; if value_.is_string() { break 'brk value_; @@ -4956,9 +4961,15 @@ pub mod formatter { ); } - if let Some(message_value) = - value.fast_get(self.global_this, jsc::BuiltinName::Message)? - { + let message_value = + match value.fast_get(self.global_this, jsc::BuiltinName::Message) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(message_value) = message_value { if message_value.is_string() { if !self.single_line { self.write_indent(writer_).expect("unreachable"); @@ -4995,9 +5006,14 @@ pub mod formatter { pf!(""), pf!("") ); - let data: JSValue = value - .fast_get(self.global_this, jsc::BuiltinName::Data)? - .unwrap_or(JSValue::UNDEFINED); + let data: JSValue = + match value.fast_get(self.global_this, jsc::BuiltinName::Data) { + Ok(v) => v.unwrap_or(JSValue::UNDEFINED), + Err(_) => { + self.global_this.clear_exception(); + JSValue::UNDEFINED + } + }; let tag = Tag::get_advanced(data, self.global_this, self.tag_opts())?; self.format::(tag, writer_, data, self.global_this)?; if self.failed { @@ -5009,9 +5025,15 @@ pub mod formatter { } } EventType::ErrorEvent => { - if let Some(error_value) = - value.fast_get(self.global_this, jsc::BuiltinName::Error)? - { + let error_value = + match value.fast_get(self.global_this, jsc::BuiltinName::Error) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(error_value) = error_value { if !self.single_line { self.write_indent(writer_).expect("unreachable"); } diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index fc224cc8f47..311e1f2e709 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -5218,11 +5218,15 @@ impl VirtualMachine { allow_ansi_color, allow_side_effects, }; - // `getErrorsProperty` is - // `getDirect` (own data prop, nothrow); `for_each` may throw, in - // which case the error is swallowed. + // `getDirect` may hand back empty or a GetterSetter; `is_object()` rejects both. let errors = value.get_errors_property(global_ref); - let _ = errors.for_each(global_ref, (&raw mut ctx).cast(), agg_iter); + if errors.is_object() + && errors + .for_each(global_ref, (&raw mut ctx).cast(), agg_iter) + .is_err() + { + global_ref.clear_exception(); + } return; } diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index 414376f6f40..921a598faa6 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -1829,9 +1829,13 @@ impl<'a> Formatter<'a> { } Tag::Event => { let event_type_value: JSValue = 'brk: { - let value_: JSValue = match value.get(self.global_this, "type")? { - Some(v) => v, - None => break 'brk JSValue::UNDEFINED, + let value_: JSValue = match value.get(self.global_this, "type") { + Ok(Some(v)) => v, + Ok(None) => break 'brk JSValue::UNDEFINED, + Err(_) => { + self.global_this.clear_exception(); + break 'brk JSValue::UNDEFINED; + } }; if value_.is_string() { break 'brk value_; @@ -1878,9 +1882,16 @@ impl<'a> Formatter<'a> { pretty_fmt_const!(ENABLE_ANSI_COLORS, ""), )); - if let Some(message_value) = - value.fast_get(self.global_this, jsc::BuiltinName::Message)? + let message_value = match value + .fast_get(self.global_this, jsc::BuiltinName::Message) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(message_value) = message_value { if message_value.is_string() { self.write_indent(writer.ctx).expect("unreachable"); writer.print(format_args!( @@ -1907,9 +1918,15 @@ impl<'a> Formatter<'a> { pretty_fmt_const!(ENABLE_ANSI_COLORS, ""), pretty_fmt_const!(ENABLE_ANSI_COLORS, ""), )); - let data: JSValue = value - .fast_get(self.global_this, jsc::BuiltinName::Data)? - .unwrap_or(JSValue::UNDEFINED); + let data: JSValue = match value + .fast_get(self.global_this, jsc::BuiltinName::Data) + { + Ok(v) => v.unwrap_or(JSValue::UNDEFINED), + Err(_) => { + self.global_this.clear_exception(); + JSValue::UNDEFINED + } + }; let tag = Tag::get(data, self.global_this)?; self.format::( @@ -1918,9 +1935,16 @@ impl<'a> Formatter<'a> { writer.write_all(b", \n"); } EventType::ErrorEvent => { - if let Some(data) = - value.fast_get(self.global_this, jsc::BuiltinName::Error)? + let error_value = match value + .fast_get(self.global_this, jsc::BuiltinName::Error) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(data) = error_value { self.write_indent(writer.ctx).expect("unreachable"); writer.print(format_args!( "{}error{}:{} ", diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 5c91f3dbe05..cb56f3db545 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness"; import { join } from "path"; import util from "util"; it("prototype", () => { @@ -205,6 +205,107 @@ it("MessageEvent with deleted data", () => { ); }); +it("Event subclass with a throwing getter does not make Bun.inspect throw", () => { + class ThrowType extends Event { + get type() { + throw new Error("type-getter-boom"); + } + } + const typeOut = Bun.inspect(new ThrowType("t")); + expect(typeOut).toContain("type: [Getter]"); + expect(typeOut).not.toContain("type-getter-boom"); + expect(() => Bun.inspect({ payload: [new ThrowType("t")] })).not.toThrow(); + + class ThrowData extends MessageEvent { + get data() { + throw new Error("data-getter-boom"); + } + } + expect(Bun.inspect(new ThrowData("message", { data: "p" }))).toBe( + `MessageEvent {\n type: "message",\n data: undefined,\n}`, + ); + + class ThrowError extends ErrorEvent { + get error() { + throw new Error("error-getter-boom"); + } + get message() { + throw new Error("message-getter-boom"); + } + } + expect(Bun.inspect(new ThrowError("error", { message: "m", error: new Error("i") }))).toBe( + `ErrorEvent {\n type: "error",\n}`, + ); + + // Own-instance accessors (not subclass) on the Event branch reads. + const me = new MessageEvent("message", { data: "p" }); + Object.defineProperty(me, "data", { + get() { + throw new Error("own-data-boom"); + }, + configurable: true, + }); + expect(Bun.inspect(me)).toBe(`MessageEvent {\n type: "message",\n data: undefined,\n}`); + expect(Bun.inspect({ nested: me })).toContain("data: undefined"); +}); + +it("AggregateError with a hostile 'errors' property does not make Bun.inspect throw", () => { + // Accessor own prop: getDirect returns the GetterSetter cell, for_each throws. + const a = new AggregateError([new Error("x")], "agg"); + Object.defineProperty(a, "errors", { + get() { + throw new Error("errors-getter-boom"); + }, + configurable: true, + }); + expect(() => Bun.inspect(a)).not.toThrow(); + expect(() => Bun.inspect({ nested: a })).not.toThrow(); + + // Non-iterable data prop: for_each throws TypeError. + const b = new AggregateError([new Error("x")], "agg"); + b.errors = { not: "iterable" }; + expect(() => Bun.inspect(b)).not.toThrow(); + + // Deleted own prop: getDirect returns empty; used to segfault in for_each. + const c = new AggregateError([new Error("x")], "agg"); + delete c.errors; + expect(() => Bun.inspect(c)).not.toThrow(); +}); + +it("Event subclass with a throwing getter does not make toMatchSnapshot fail", async () => { + using dir = tempDir("inspect-event-snapshot", { + "snap.test.js": ` + import { test, expect } from "bun:test"; + test("type", () => { + class E extends Event { get type() { throw new Error("type-getter-boom"); } } + expect(new E("t")).toMatchSnapshot(); + }); + test("data", () => { + class M extends Event { get data() { throw new Error("data-getter-boom"); } } + expect(new M("message")).toMatchSnapshot(); + }); + test("error", () => { + class R extends Event { + get error() { throw new Error("error-getter-boom"); } + get message() { throw new Error("message-getter-boom"); } + } + expect(new R("error")).toMatchSnapshot(); + }); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "--update-snapshots", "snap.test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const all = stdout + stderr; + expect(all).not.toContain("Failed to pretty format"); + expect(all).toContain("3 pass"); + expect(exitCode).toBe(0); +}); + // https://github.com/oven-sh/bun/issues/561 it("TypedArray prints", () => { for (let TypedArray of [