Merge pull request #1365 from lightpanda-io/try_catch_caught

Try catch caught
This commit is contained in:
Karl Seguin
2026-01-14 09:59:19 +00:00
committed by GitHub
17 changed files with 115 additions and 115 deletions

View File

@@ -804,9 +804,8 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
// it AFTER. // it AFTER.
const ms_to_next_task = try scheduler.run(); const ms_to_next_task = try scheduler.run();
if (try_catch.hasCaught()) { if (try_catch.caught(self.call_arena)) |caught| {
const msg = (try try_catch.err(self.arena)) orelse "unknown"; log.info(.js, "page wait", .{ .caught = caught, .src = "scheduler" });
log.info(.js, "page wait", .{ .err = msg, .src = "scheduler" });
} }
const http_active = http_client.active; const http_active = http_client.active;
@@ -1992,9 +1991,9 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
self._upgrading_element = node; self._upgrading_element = node;
defer self._upgrading_element = prev_upgrading; defer self._upgrading_element = prev_upgrading;
var result: JS.Function.Result = undefined; var caught: JS.TryCatch.Caught = undefined;
_ = def.constructor.newInstance(&result) catch |err| { _ = def.constructor.newInstance(&caught) catch |err| {
log.warn(.js, "custom element constructor", .{ .name = name, .err = err }); log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught });
return node; return node;
}; };

View File

@@ -834,12 +834,10 @@ pub const Script = struct {
return; return;
} }
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown"; const caught = try_catch.caughtOrError(page.call_arena, error.Unknown);
log.warn(.js, "eval script", .{ log.warn(.js, "eval script", .{
.url = url, .url = url,
.err = msg, .caught = caught,
.stack = try_catch.stack(page.call_arena) catch null,
.line = try_catch.sourceLineNumber() orelse 0,
.cacheable = cacheable, .cacheable = cacheable,
}); });
@@ -859,13 +857,12 @@ pub const Script = struct {
return; return;
}; };
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{event}, &result) catch { cb.tryCall(void, .{event}, &caught) catch {
log.warn(.js, "script callback", .{ log.warn(.js, "script callback", .{
.url = self.url, .url = self.url,
.type = typ, .type = typ,
.err = result.exception, .caught = caught,
.stack = result.stack,
}); });
}; };
} }

View File

@@ -1386,15 +1386,13 @@ fn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptM
try_catch.init(self); try_catch.init(self);
defer try_catch.deinit(); defer try_catch.deinit();
break :blk self.module(true, ms.src(), state.specifier, true) catch { break :blk self.module(true, ms.src(), state.specifier, true) catch |err| {
const ex = try_catch.exception(self.call_arena) catch |err| @errorName(err) orelse "unknown error"; const caught = try_catch.caughtOrError(self.call_arena, err);
log.err(.js, "module compilation failed", .{ log.err(.js, "module compilation failed", .{
.caught = caught,
.specifier = state.specifier, .specifier = state.specifier,
.exception = ex,
.stack = try_catch.stack(self.call_arena) catch null,
.line = try_catch.sourceLineNumber() orelse 0,
}); });
_ = state.resolver.reject("dynamic compilation failure", self.newString(ex)); _ = state.resolver.reject("dynamic compilation failure", self.newString(caught.exception orelse ""));
return; return;
}; };
}; };

View File

@@ -50,7 +50,7 @@ pub fn withThis(self: *const Function, value: anytype) !Function {
}; };
} }
pub fn newInstance(self: *const Function, result: *Result) !js.Object { pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
const ctx = self.ctx; const ctx = self.ctx;
var try_catch: js.TryCatch = undefined; var try_catch: js.TryCatch = undefined;
@@ -60,14 +60,7 @@ pub fn newInstance(self: *const Function, result: *Result) !js.Object {
// This creates a new instance using this Function as a constructor. // This creates a new instance using this Function as a constructor.
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{})); // const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse { const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse {
if (try_catch.hasCaught()) { caught.* = try_catch.caughtOrError(ctx.call_arena, error.Unknown);
const allocator = ctx.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
} else {
result.stack = null;
result.exception = "???";
}
return error.JsConstructorFailed; return error.JsConstructorFailed;
}; };
@@ -81,25 +74,18 @@ pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
return self.callWithThis(T, self.getThis(), args); return self.callWithThis(T, self.getThis(), args);
} }
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T { pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
return self.tryCallWithThis(T, self.getThis(), args, result); return self.tryCallWithThis(T, self.getThis(), args, caught);
} }
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T { pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
var try_catch: js.TryCatch = undefined; var try_catch: js.TryCatch = undefined;
try_catch.init(self.ctx); try_catch.init(self.ctx);
defer try_catch.deinit(); defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| { return self.callWithThis(T, this, args) catch |err| {
if (try_catch.hasCaught()) { caught.* = try_catch.caughtOrError(self.ctx.call_arena, err);
const allocator = self.ctx.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
} else {
result.stack = null;
result.exception = @errorName(err);
}
return err; return err;
}; };
} }

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
// //
// Francis Bouvier <francis@lightpanda.io> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -36,53 +36,72 @@ pub fn hasCaught(self: TryCatch) bool {
return v8.v8__TryCatch__HasCaught(&self.handle); return v8.v8__TryCatch__HasCaught(&self.handle);
} }
// the caller needs to deinit the string returned pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 { if (!self.hasCaught()) {
const msg_value = v8.v8__TryCatch__Exception(&self.handle) orelse return null;
const msg = js.Value{ .ctx = self.ctx, .handle = msg_value };
return try self.ctx.valueToString(msg, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
const ctx = self.ctx;
const s_value = v8.v8__TryCatch__StackTrace(&self.handle, ctx.handle) orelse return null;
const s = js.Value{ .ctx = ctx, .handle = s_value };
return try ctx.valueToString(s, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
const ctx = self.ctx;
const msg = v8.v8__TryCatch__Message(&self.handle) orelse return null;
const source_line_handle = v8.v8__Message__GetSourceLine(msg, ctx.handle) orelse return null;
return try ctx.jsStringToZig(source_line_handle, .{ .allocator = allocator });
}
pub fn sourceLineNumber(self: TryCatch) ?u32 {
const ctx = self.ctx;
const msg = v8.v8__TryCatch__Message(&self.handle) orelse return null;
const line = v8.v8__Message__GetLineNumber(msg, ctx.handle);
if (line < 0) {
return null; return null;
} }
return @intCast(line);
const ctx = self.ctx;
var hs: js.HandleScope = undefined;
hs.init(ctx.isolate);
defer hs.deinit();
const line: ?u32 = blk: {
const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
const l = v8.v8__Message__GetLineNumber(handle, ctx.handle);
break :blk if (l < 0) null else @intCast(l);
};
const exception: ?[]const u8 = blk: {
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
break :blk ctx.valueToString(.{ .ctx = ctx, .handle = handle }, .{ .allocator = allocator }) catch |err| @errorName(err);
};
const stack: ?[]const u8 = blk: {
const handle = v8.v8__TryCatch__StackTrace(&self.handle, ctx.handle) orelse break :blk null;
break :blk ctx.valueToString(.{ .ctx = ctx, .handle = handle }, .{ .allocator = allocator }) catch |err| @errorName(err);
};
return .{
.line = line,
.stack = stack,
.caught = true,
.exception = exception,
};
} }
// a shorthand method to return either the entire stack message pub fn caughtOrError(self: TryCatch, allocator: Allocator, err: anyerror) Caught {
// or just the exception message return self.caught(allocator) orelse .{
// - in Debug mode return the stack if available .caught = false,
// - otherwise return the exception if available .line = null,
// the caller needs to deinit the string returned .stack = null,
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 { .exception = @errorName(err),
if (comptime @import("builtin").mode == .Debug) { };
if (try self.stack(allocator)) |msg| {
return msg;
}
}
return try self.exception(allocator);
} }
pub fn deinit(self: *TryCatch) void { pub fn deinit(self: *TryCatch) void {
v8.v8__TryCatch__DESTRUCT(&self.handle); v8.v8__TryCatch__DESTRUCT(&self.handle);
} }
pub const Caught = struct {
line: ?u32,
caught: bool,
stack: ?[]const u8,
exception: ?[]const u8,
pub fn format(self: Caught, writer: *std.Io.Writer) !void {
const separator = @import("../../log.zig").separator();
try writer.print("{s}exception: {?s}", .{ separator, self.exception });
try writer.print("{s}stack: {?s}", .{ separator, self.stack });
try writer.print("{s}line: {?d}", .{ separator, self.line });
try writer.print("{s}caught: {any}", .{ separator, self.caught });
}
pub fn logFmt(self: Caught, comptime prefix: []const u8, writer: anytype) !void {
try writer.write(prefix ++ ".exception", self.exception orelse "???");
try writer.write(prefix ++ ".stack", self.stack orelse "na");
try writer.write(prefix ++ ".line", self.line);
try writer.write(prefix ++ ".caught", self.caught);
}
};

View File

@@ -174,9 +174,9 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
page._upgrading_element = node; page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading; defer page._upgrading_element = prev_upgrading;
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
_ = definition.constructor.newInstance(&result) catch |err| { _ = definition.constructor.newInstance(&caught) catch |err| {
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err }); log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught });
return error.CustomElementUpgradeFailed; return error.CustomElementUpgradeFailed;
}; };

View File

@@ -245,9 +245,9 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
} }
const entries = try self.takeRecords(page); const entries = try self.takeRecords(page);
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
self._callback.tryCall(void, .{ entries, self }, &result) catch |err| { self._callback.tryCall(void, .{ entries, self }, &caught) catch |err| {
log.err(.page, "IntsctObserver.deliverEntries", .{ .err = result.exception, .stack = result.stack }); log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
return err; return err;
}; };
} }

View File

@@ -248,9 +248,9 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
// Take a copy of the records and clear the list before calling callback // Take a copy of the records and clear the list before calling callback
// This ensures mutations triggered during the callback go into a fresh list // This ensures mutations triggered during the callback go into a fresh list
const records = try self.takeRecords(page); const records = try self.takeRecords(page);
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
self._callback.tryCall(void, .{ records, self }, &result) catch |err| { self._callback.tryCall(void, .{ records, self }, &caught) catch |err| {
log.err(.page, "MutObserver.deliverRecords", .{ .err = result.exception, .stack = result.stack }); log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
return err; return err;
}; };
} }

View File

@@ -151,9 +151,9 @@ pub inline fn hasRecords(self: *const PerformanceObserver) bool {
/// Runs the PerformanceObserver's callback with records; emptying it out. /// Runs the PerformanceObserver's callback with records; emptying it out.
pub fn dispatch(self: *PerformanceObserver, page: *Page) !void { pub fn dispatch(self: *PerformanceObserver, page: *Page) !void {
const records = try self.takeRecords(page); const records = try self.takeRecords(page);
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
self._callback.tryCall(void, .{ EntryList{ ._entries = records }, self }, &result) catch |err| { self._callback.tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
log.err(.page, "PerfObserver.dispatch", .{ .err = result.exception, .stack = result.stack }); log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
return err; return err;
}; };
} }

View File

@@ -189,9 +189,9 @@ pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, page
if (gop.found_existing) { if (gop.found_existing) {
continue; continue;
} }
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{ token, i, self }, &result) catch { cb.tryCall(void, .{ token, i, self }, &caught) catch {
log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack, .source = "DOMTokenList" }); log.debug(.js, "forEach callback", .{ .caught = caught, .source = "DOMTokenList" });
return; return;
}; };
i += 1; i += 1;

View File

@@ -83,9 +83,9 @@ pub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void {
return; return;
} }
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{ next.value, i, self }, &result) catch { cb.tryCall(void, .{ next.value, i, self }, &caught) catch {
log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack, .source = "nodelist" }); log.debug(.js, "forEach callback", .{ .caught = caught, .source = "nodelist" });
return; return;
}; };
} }

View File

@@ -195,9 +195,9 @@ pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void {
page._upgrading_element = node; page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading; defer page._upgrading_element = prev_upgrading;
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
_ = definition.constructor.newInstance(&result) catch |err| { _ = definition.constructor.newInstance(&caught) catch |err| {
log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err }); log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err, .caught = caught });
return; return;
}; };
} }

View File

@@ -78,9 +78,9 @@ pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void {
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_; const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
for (self._list._entries.items) |entry| { for (self._list._entries.items) |entry| {
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{ entry.value.str(), entry.name.str(), self }, &result) catch { cb.tryCall(void, .{ entry.value.str(), entry.name.str(), self }, &caught) catch {
log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack, .source = "headers" }); log.debug(.js, "forEach callback", .{ .caught = caught, .source = "headers" });
}; };
} }
} }

View File

@@ -333,10 +333,10 @@ fn writeString(comptime format: Format, value: []const u8, writer: *std.Io.Write
pub const LogFormatWriter = struct { pub const LogFormatWriter = struct {
writer: *std.Io.Writer, writer: *std.Io.Writer,
pub fn write(self: LogFormatWriter, key: []const u8, value: []const u8) !void { pub fn write(self: LogFormatWriter, key: []const u8, value: anytype) !void {
const writer = self.writer; const writer = self.writer;
try writer.print(" {s}=", .{key}); try writer.print(" {s}=", .{key});
try writeString(.logfmt, value, writer); try writeValue(.logfmt, value, writer);
} }
}; };

View File

@@ -94,9 +94,8 @@ pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void {
_ = session.wait(2000); _ = session.wait(2000);
js_context.eval("testing.assertOk()", "testing.assertOk()") catch |err| { js_context.eval("testing.assertOk()", "testing.assertOk()") catch |err| {
const msg = try_catch.err(allocator) catch @errorName(err) orelse "unknown"; const caught = try_catch.caughtOrError(allocator, err);
std.debug.print("{s}: test failure\nError: {f}\n", .{ file, caught });
std.debug.print("{s}: test failure\nError: {s}\n", .{ file, msg });
return err; return err;
}; };
} }

View File

@@ -125,13 +125,15 @@ fn run(
// Check the final test status. // Check the final test status.
js_context.eval("report.status", "teststatus") catch |err| { js_context.eval("report.status", "teststatus") catch |err| {
err_out.* = try_catch.err(arena) catch @errorName(err) orelse "unknown"; const caught = try_catch.caughtOrError(arena, err);
err_out.* = caught.exception;
return err; return err;
}; };
// return the detailed result. // return the detailed result.
const value = js_context.exec("report.log", "report") catch |err| { const value = js_context.exec("report.log", "report") catch |err| {
err_out.* = try_catch.err(arena) catch @errorName(err) orelse "unknown"; const caught = try_catch.caughtOrError(arena, err);
err_out.* = caught.exception;
return err; return err;
}; };

View File

@@ -407,8 +407,8 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
test_browser.runMicrotasks(); test_browser.runMicrotasks();
js_context.eval("testing.assertOk()", "testing.assertOk()") catch |err| { js_context.eval("testing.assertOk()", "testing.assertOk()") catch |err| {
const msg = try_catch.err(arena_allocator) catch @errorName(err) orelse "unknown"; const caught = try_catch.caughtOrError(arena_allocator, err);
std.debug.print("{s}: test failure\nError: {s}\n", .{ test_file, msg }); std.debug.print("{s}: test failure\nError: {f}\n", .{ test_file, caught });
return err; return err;
}; };
} }