Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
3 changes: 2 additions & 1 deletion src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5799,8 +5799,9 @@ extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJS
ZigString key = toZigString(name);

JSC::EnsureStillAliveScope ensureStillAliveScope(propertyValue);
// TODO: properly propagate exception upwards
iter(globalObject, arg2, &key, JSC::JSValue::encode(propertyValue), property.isSymbol(), property.isPrivateName());
// Propagate exceptions from callbacks.
RETURN_IF_EXCEPTION(scope, void());
}
properties.releaseData();
}
Expand Down
43 changes: 43 additions & 0 deletions test/js/bun/test/snapshot-tests/bun-snapshots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,47 @@ describe("toMatchSnapshot errors", () => {
expect({ a: 4 }).toMatchSnapshot({ a: expect.any("not a constructor") });
}).toThrow();
});

it("should throw if formatting a nested value throws, instead of leaving that property out", () => {
// The snapshot formatter reads `$$typeof` off every object to detect React elements.
const reads: string[] = [];
const formattable = (name: string) => ({
get $$typeof(): unknown {
reads.push(name);
return undefined;
},
});
const value = {
x: {
a: {
get $$typeof(): unknown {
reads.push("a");
throw new Error("boom");
},
},
b: formattable("b"),
},
y: formattable("y"),
};

// The matcher reports formatting failures with its own message rather than
// rethrowing the getter's error.
expect(() => {
// This is what used to get recorded: `a` dropped, both walks carried on.
expect(value).toMatchInlineSnapshot(`
{
"x": {
"b": {
"$$typeof": [native code],
},
},
"y": {
"$$typeof": [native code],
},
}
`);
}).toThrow("Failed to pretty format value");
// Neither the rest of `x` nor the rest of `value` was formatted.
expect(reads).toEqual(["a"]);
});
});
75 changes: 75 additions & 0 deletions test/js/bun/util/inspect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -928,3 +928,78 @@ describe.skipIf(!isASAN)("object mutated while being formatted", () => {
expect(exitCode).toBe(0);
});
});

describe("a nested value throws while formatting with { sorted: true }", () => {
const custom = Symbol.for("nodejs.util.inspect.custom");
const throwing = (visited, name) => ({
[custom]() {
visited.push(name);
throw new Error("boom");
},
});
const formattable = (visited, name) => ({
[custom]() {
visited.push(name);
return name;
},
});

it("throws and stops the walk at the throwing property, like the unsorted walk", () => {
const visited = [];
// `a` is neither first in insertion order nor last in sort order.
const obj = { c: formattable(visited, "c"), a: throwing(visited, "a"), b: formattable(visited, "b") };

expect(() => Bun.inspect(obj, { sorted: true })).toThrow("boom");
expect(visited).toEqual(["a"]);

visited.length = 0;
expect(() => Bun.inspect(obj)).toThrow("boom");
expect(visited).toEqual(["c", "a"]);
});

it("also stops the walks of the enclosing objects", () => {
const visited = [];
const obj = {
x: { a: throwing(visited, "a"), b: formattable(visited, "b") },
y: formattable(visited, "y"),
};

expect(() => Bun.inspect(obj, { sorted: true })).toThrow("boom");
expect(visited).toEqual(["a"]);
});

it("Bun.inspect.table", () => {
const visited = [];
const rows = [{ cell: { a: throwing(visited, "a"), b: formattable(visited, "b") } }];

expect(() => Bun.inspect.table(rows, { sorted: true })).toThrow("boom");
expect(visited).toEqual(["a"]);
});

it("does not read the following properties while the exception is pending", async () => {
// Most of the Bun object's properties are initialized lazily by a native
// callback the first time they are read. "A0" sorts right before one of them
// (Bun.Archive), so reading on with the exception from "A0" still pending
// trips the native callback's exception check in debug/ASAN builds.
const fixture = `
Bun.A0 = { [Symbol.for("nodejs.util.inspect.custom")]() { throw new Error("boom"); } };
try {
Bun.inspect(Bun, { sorted: true });
console.log("returned");
} catch (e) {
console.log("caught:", e.message);
}
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", fixture],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toBe("caught: boom\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
});