From b573e44fe1ea231bae936dc25e7df3724cc440c7 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Tue, 5 May 2026 18:16:52 +0200 Subject: [PATCH] events: report listener exceptions instead of halting dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per DOM §2.9 step 4 substep 8 ("Inner invoke"), an exception thrown by a listener callback must be reported to the realm's global, not propagated out of `dispatchEvent`. Subsequent listeners on the same target and the rest of the capture/bubble walk must still run. Both `EventManager.dispatchPhase` (DOM targets) and `EventManagerBase.dispatchDirect` (Window/XHR/AbortSignal/etc.) wrapped the V8 callback invocation in raw `try`, so a listener throw aborted the whole dispatch and surfaced as an uncaught exception at the `Runtime.evaluate` boundary. The inline-handler invocation a few lines above already used `catch |err| log.warn(...)`; this just extends the same shape to the listener switch. Closes #2367 --- src/browser/EventManager.zig | 23 ++++++-- src/browser/EventManagerBase.zig | 22 ++++++-- src/browser/tests/events.html | 92 ++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 8 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 77cb47b0..cd44a7e8 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -388,16 +388,31 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe event._target = getAdjustedTarget(original_target, current_target); } + // Per DOM §2.9 step 4 substep 8 ("Inner invoke"), a listener callback that + // throws must have its exception *reported* to the global error handler, + // not propagated to the dispatch caller — subsequent listeners on the same + // target and the rest of the propagation path must still run. Mirrors the + // catch on the inline-handler invocation in dispatchDirect. switch (listener.function) { - .value => |value| try local.toLocal(value).callWithThis(void, current_target, .{event}), + .value => |value| local.toLocal(value).callWithThis(void, current_target, .{event}) catch |err| { + log.warn(.event, "listener", .{ .err = err }); + }, .string => |string| { const str = try frame.call_arena.dupeZ(u8, string.str()); - try local.eval(str, null); + local.eval(str, null) catch |err| { + log.warn(.event, "listener string", .{ .err = err }); + }; }, .object => |obj_global| { const obj = local.toLocal(obj_global); - if (try obj.getFunction("handleEvent")) |handleEvent| { - try handleEvent.callWithThis(void, obj, .{event}); + const handle_event = obj.getFunction("handleEvent") catch |err| blk: { + log.warn(.event, "listener handleEvent", .{ .err = err }); + break :blk null; + }; + if (handle_event) |handleEvent| { + handleEvent.callWithThis(void, obj, .{event}) catch |err| { + log.warn(.event, "listener object", .{ .err = err }); + }; } }, } diff --git a/src/browser/EventManagerBase.zig b/src/browser/EventManagerBase.zig index 8e13ecd5..7ac622ae 100644 --- a/src/browser/EventManagerBase.zig +++ b/src/browser/EventManagerBase.zig @@ -305,16 +305,30 @@ pub fn dispatchDirect( event._current_target = target; + // Per DOM §2.9 step 4 substep 8 ("Inner invoke"), a listener callback that + // throws must have its exception *reported*, not propagated to the dispatch + // caller — subsequent listeners must still run. Same shape as the catch on + // the property-handler invocation above and on EventManager.dispatchPhase. switch (listener.function) { - .value => |value| try ls.local.toLocal(value).callWithThis(void, target, .{event}), + .value => |value| ls.local.toLocal(value).callWithThis(void, target, .{event}) catch |err| { + log.warn(.event, opts.context, .{ .err = err }); + }, .string => |string| { const str = try arena.dupeZ(u8, string.str()); - try ls.local.eval(str, null); + ls.local.eval(str, null) catch |err| { + log.warn(.event, opts.context, .{ .err = err }); + }; }, .object => |obj_global| { const obj = ls.local.toLocal(obj_global); - if (try obj.getFunction("handleEvent")) |handleEvent| { - try handleEvent.callWithThis(void, obj, .{event}); + const handle_event = obj.getFunction("handleEvent") catch |err| blk: { + log.warn(.event, opts.context, .{ .err = err }); + break :blk null; + }; + if (handle_event) |handleEvent| { + handleEvent.callWithThis(void, obj, .{event}) catch |err| { + log.warn(.event, opts.context, .{ .err = err }); + }; } }, } diff --git a/src/browser/tests/events.html b/src/browser/tests/events.html index 80d41707..eae7f883 100644 --- a/src/browser/tests/events.html +++ b/src/browser/tests/events.html @@ -762,3 +762,95 @@ testing.expectEqual(2, calls.length); } + +
+ + +
+ + +