-
Notifications
You must be signed in to change notification settings - Fork 4.4k
fix(console): print AggregateError wrapper instead of dropping it #29163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
1955b21
deba3bc
b2f60ed
f2daa46
cd7672b
9f1c831
d3dc440
acfc93a
4fcac71
e7f4b9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2454,31 +2454,10 @@ pub fn printErrorlikeObject( | |
| } | ||
| } | ||
|
|
||
| if (value.isAggregateError(this.global)) { | ||
| const AggregateErrorIterator = struct { | ||
| writer: Writer, | ||
| current_exception_list: ?*ExceptionList = null, | ||
| formatter: *ConsoleObject.Formatter, | ||
|
|
||
| pub fn iteratorWithColor(vm: *VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.c) void { | ||
| iterator(vm, globalObject, nextValue, ctx.?, true); | ||
| } | ||
| pub fn iteratorWithOutColor(vm: *VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.c) void { | ||
| iterator(vm, globalObject, nextValue, ctx.?, false); | ||
| } | ||
| fn iterator(_: *VM, _: *JSGlobalObject, nextValue: JSValue, ctx: ?*anyopaque, comptime color: bool) void { | ||
| const this_ = @as(*@This(), @ptrFromInt(@intFromPtr(ctx))); | ||
| VirtualMachine.get().printErrorlikeObject(nextValue, null, this_.current_exception_list, this_.formatter, Writer, this_.writer, color, allow_side_effects); | ||
| } | ||
| }; | ||
| var iter = AggregateErrorIterator{ .writer = writer, .current_exception_list = exception_list, .formatter = formatter }; | ||
| if (comptime allow_ansi_color) { | ||
| value.getErrorsProperty(this.global).forEach(this.global, &iter, AggregateErrorIterator.iteratorWithColor) catch return; // TODO: properly propagate exception upwards | ||
| } else { | ||
| value.getErrorsProperty(this.global).forEach(this.global, &iter, AggregateErrorIterator.iteratorWithOutColor) catch return; // TODO: properly propagate exception upwards | ||
| } | ||
| return; | ||
| } | ||
| // AggregateError is handled by printErrorInstance, which prints the | ||
| // wrapper (name / message / stack) and then fans out the non-enumerable | ||
| // `errors` array via the errors_to_append queue — see the cause block | ||
| // there for the same pattern. | ||
|
|
||
| was_internal = this.printErrorFromMaybePrivateData( | ||
| value, | ||
|
|
@@ -3289,6 +3268,26 @@ fn printErrorInstance( | |
| } | ||
| } | ||
| } | ||
|
|
||
| // `AggregateError.errors` is a non-enumerable own property, so the | ||
| // property iterator above skips it. Walk it manually and queue each | ||
| // entry (of any type — Promise.any can reject with non-Error values) | ||
| // so the wrapper gets printed first, matching Node's `[errors]: [...]` | ||
| // format instead of the errors replacing it. | ||
| if (error_instance.isAggregateError(this.global)) { | ||
| const AggregateErrorsIterator = struct { | ||
| errors_to_append: *std.array_list.Managed(JSValue), | ||
|
|
||
| pub fn visit(_: *VM, _: *JSGlobalObject, ctx: ?*anyopaque, next_value: JSValue) callconv(.c) void { | ||
| const self: *@This() = @ptrCast(@alignCast(ctx.?)); | ||
| next_value.protect(); | ||
| bun.handleOom(self.errors_to_append.append(next_value)); | ||
| } | ||
| }; | ||
| var errors_iter = AggregateErrorsIterator{ .errors_to_append = &errors_to_append }; | ||
| const errors_array = error_instance.getErrorsProperty(this.global); | ||
| try errors_array.forEach(this.global, &errors_iter, AggregateErrorsIterator.visit); | ||
|
Comment on lines
+3268
to
+3280
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The drain loop that replays queued Extended reasoning...What the bug is and how it manifests The new The specific code path that triggers it
current.* = switch (msg.metadata) {
.build => bun.api.BuildMessage.create(...),
.resolve => bun.api.ResolveMessage.create(...),
};
ret.* = ErrorableResolvedSource.err(err, globalThis.createAggregateError(errors, ...));When Why existing code doesn't prevent it The specialized formatting path for History of the regression Commit cd7672b had correctly fixed the drain loop to call Impact Any user who runs Bun on a script that dynamically imports a module with 2+ syntax errors (or uses Step-by-step proof
Fix In the drain loop (~line 3342), replace |
||
| } | ||
|
robobun marked this conversation as resolved.
|
||
| } else if (mode == .js and error_instance != .zero) { | ||
| // If you do reportError([1,2,3]] we should still show something at least. | ||
| const tag = try jsc.Formatter.Tag.getAdvanced( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| // https://github.com/oven-sh/bun/issues/29157 | ||
|
|
||
| import { expect, test } from "bun:test"; | ||
| import { bunEnv, bunExe } from "harness"; | ||
|
|
||
| test("console.dir on AggregateError prints the wrapper, not just the inner errors", async () => { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [ | ||
| bunExe(), | ||
| "-e", | ||
| `const err = new AggregateError([new Error("child1"), new Error("child2")], "wrapper"); | ||
| console.dir(err);`, | ||
| ], | ||
| env: bunEnv, | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stdout).toContain("AggregateError"); | ||
| expect(stdout).toContain("wrapper"); | ||
|
robobun marked this conversation as resolved.
|
||
| expect(stdout).toContain("child1"); | ||
| expect(stdout).toContain("child2"); | ||
| expect(stderr).toBe(""); | ||
| expect(exitCode).toBe(0); | ||
|
robobun marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| test("Promise.any rejection surfaces as an AggregateError, not a stray inner error", async () => { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), "-e", `await Promise.any([Promise.reject(new Error("inner"))]).catch(console.dir);`], | ||
| env: bunEnv, | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stdout).toContain("AggregateError"); | ||
| expect(stdout).toContain("inner"); | ||
| expect(stderr).toBe(""); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test("uncaught AggregateError rejection still shows the wrapper", async () => { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), "-e", `await Promise.any([Promise.reject(new Error("a")), Promise.reject(new Error("b"))]);`], | ||
| env: bunEnv, | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stderr).toContain("AggregateError"); | ||
| expect(stderr).toContain("a"); | ||
| expect(stderr).toContain("b"); | ||
| expect(exitCode).not.toBe(0); | ||
|
claude[bot] marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| test("AggregateError with a cause chain prints wrapper + errors + cause", async () => { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [ | ||
| bunExe(), | ||
| "-e", | ||
| `const cause = new Error("root cause"); | ||
| const err = new AggregateError([new Error("child")], "wrapper", { cause }); | ||
| console.dir(err);`, | ||
| ], | ||
| env: bunEnv, | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stdout).toContain("AggregateError"); | ||
| expect(stdout).toContain("wrapper"); | ||
| expect(stdout).toContain("child"); | ||
| expect(stdout).toContain("root cause"); | ||
| expect(stderr).toBe(""); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.