return correct errors from promises

This commit is contained in:
Halil Durak
2026-03-19 12:24:39 +03:00
parent 93e239f682
commit 94190f93af
11 changed files with 72 additions and 44 deletions

View File

@@ -545,13 +545,13 @@ pub fn dynamicModuleCallback(
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| { break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" }); log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
return @constCast((local.rejectPromise("Out of memory") catch return null).handle); return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
}; };
}; };
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| { const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" }); log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
return @constCast((local.rejectPromise("Out of memory") catch return null).handle); return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
}; };
const normalized_specifier = self.script_manager.?.resolveSpecifier( const normalized_specifier = self.script_manager.?.resolveSpecifier(
@@ -560,14 +560,14 @@ pub fn dynamicModuleCallback(
specifier, specifier,
) catch |err| { ) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" }); log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
return @constCast((local.rejectPromise("Out of memory") catch return null).handle); return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
}; };
const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: { const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: {
log.err(.js, "dynamic module callback", .{ log.err(.js, "dynamic module callback", .{
.err = err, .err = err,
}); });
break :blk local.rejectPromise("Failed to load module") catch return null; break :blk local.rejectPromise(.{ .generic_error = "Out of memory" });
}; };
return @constCast(promise.handle); return @constCast(promise.handle);
} }

View File

@@ -1206,9 +1206,9 @@ pub fn stackTrace(self: *const Local) !?[]const u8 {
} }
// == Promise Helpers == // == Promise Helpers ==
pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise { pub fn rejectPromise(self: *const Local, comptime kind: js.PromiseResolver.RejectError) js.Promise {
var resolver = js.PromiseResolver.init(self); var resolver = js.PromiseResolver.init(self);
resolver.reject("Local.rejectPromise", value); resolver.rejectError("Local.rejectPromise", kind);
return resolver.promise(); return resolver.promise();
} }

View File

@@ -22,6 +22,8 @@ const v8 = js.v8;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const DOMException = @import("../webapi/DOMException.zig"); const DOMException = @import("../webapi/DOMException.zig");
const DOMException = @import("../webapi/DOMException.zig");
const PromiseResolver = @This(); const PromiseResolver = @This();
local: *const js.Local, local: *const js.Local,
@@ -65,20 +67,43 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
}; };
} }
pub const RejectError = union(enum) { pub const RejectError = union(enum(u4)) {
generic: []const u8, /// Not to be confused with `DOMException`; this is bare `Error`.
generic_error: []const u8,
range_error: []const u8,
reference_error: []const u8,
syntax_error: []const u8,
type_error: []const u8, type_error: []const u8,
dom_exception: anyerror, wasm_compile_error: void, // TODO.
wasm_link_error: void, // TODO.
wasm_runtime_error: void, // TODO.
wasm_suspend_error: void, // TODO.
/// DOM exceptions are unknown to V8, belongs to web standards.
dom_exception: struct { err: anyerror },
}; };
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
const handle = switch (err) { /// Rejects the promise w/ an error object.
.type_error => |str| self.local.isolate.createTypeError(str), pub fn rejectError(
.generic => |str| self.local.isolate.createError(str), self: PromiseResolver,
comptime source: []const u8,
comptime kind: RejectError,
) void {
const handle = switch (kind) {
.generic_error => |msg| self.local.isolate.createError(msg),
.range_error => |msg| self.local.isolate.createRangeError(msg),
.reference_error => |msg| self.local.isolate.createReferenceError(msg),
.syntax_error => |msg| self.local.isolate.createSyntaxError(msg),
.type_error => |msg| self.local.isolate.createTypeError(msg),
// "Exceptional".
.dom_exception => |exception| { .dom_exception => |exception| {
self.reject(source, DOMException.fromError(exception)); self._reject(DOMException.fromError(exception.err) orelse unreachable) catch |reject_err| {
log.err(.bug, "rejectDomException", .{ .source = source, .err = reject_err, .persistent = false });
};
return; return;
}, },
inline else => unreachable,
}; };
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| { self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false }); log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
}; };

View File

@@ -125,8 +125,8 @@ pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page)
return local.resolvePromise(definition.constructor); return local.resolvePromise(definition.constructor);
} }
validateName(name) catch |err| { validateName(name) catch |err| switch (err) {
return local.rejectPromise(DOMException.fromError(err) orelse unreachable); error.SyntaxError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } }),
}; };
const gop = try self._when_defined.getOrPut(page.arena, name); const gop = try self._when_defined.getOrPut(page.arena, name);

View File

@@ -96,8 +96,8 @@ pub fn generateKey(
key_usages: []const []const u8, key_usages: []const []const u8,
page: *Page, page: *Page,
) !js.Promise { ) !js.Promise {
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch |err| { const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch {
return page.js.local.?.rejectPromise(@errorName(err)); return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
}; };
return page.js.local.?.resolvePromise(key_or_pair); return page.js.local.?.resolvePromise(key_or_pair);
@@ -112,7 +112,7 @@ pub fn exportKey(
page: *Page, page: *Page,
) !js.Promise { ) !js.Promise {
if (!key.canExportKey()) { if (!key.canExportKey()) {
return error.InvalidAccessError; return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
} }
if (std.mem.eql(u8, format, "raw")) { if (std.mem.eql(u8, format, "raw")) {
@@ -124,9 +124,10 @@ pub fn exportKey(
if (is_unsupported) { if (is_unsupported) {
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format }); log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
} }
return page.js.local.?.rejectPromise(@errorName(error.NotSupported)); return page.js.local.?.rejectPromise(.{ .type_error = "invalid format" });
} }
/// Derive a secret key from a master key. /// Derive a secret key from a master key.
@@ -148,7 +149,7 @@ pub fn deriveBits(
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name }); log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
} }
return page.js.local.?.rejectPromise(@errorName(error.NotSupported)); return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
}, },
}; };
} }
@@ -185,19 +186,19 @@ pub fn sign(
.hmac => { .hmac => {
// Verify algorithm. // Verify algorithm.
if (!algorithm.isHMAC()) { if (!algorithm.isHMAC()) {
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError)); return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
} }
// Call sign for HMAC. // Call sign for HMAC.
const result = key.signHMAC(data, page) catch |err| { const result = key.signHMAC(data, page) catch {
return page.js.local.?.rejectPromise(@errorName(err)); return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
}; };
return page.js.local.?.resolvePromise(result); return page.js.local.?.resolvePromise(result);
}, },
else => { else => {
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type }); log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError)); return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
}, },
}; };
} }
@@ -211,18 +212,20 @@ pub fn verify(
data: []const u8, // ArrayBuffer. data: []const u8, // ArrayBuffer.
page: *Page, page: *Page,
) !js.Promise { ) !js.Promise {
if (!algorithm.isHMAC()) return error.InvalidAccessError; if (!algorithm.isHMAC()) {
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
}
return switch (key._type) { return switch (key._type) {
.hmac => key.verifyHMAC(signature, data, page), .hmac => key.verifyHMAC(signature, data, page),
else => return error.InvalidAccessError, else => page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
}; };
} }
pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise { pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
const local = page.js.local.?; const local = page.js.local.?;
if (algorithm.len > 10) { if (algorithm.len > 10) {
return local.rejectPromise(DOMException.fromError(error.NotSupported)); return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
} }
const normalized = std.ascii.lowerString(&page.buf, algorithm); const normalized = std.ascii.lowerString(&page.buf, algorithm);
if (std.mem.eql(u8, normalized, "sha-1")) { if (std.mem.eql(u8, normalized, "sha-1")) {
@@ -245,7 +248,7 @@ pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray
Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{}); Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{});
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] }); return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] });
} }
return local.rejectPromise(DOMException.fromError(error.NotSupported)); return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
} }
/// Returns the desired digest by its name. /// Returns the desired digest by its name.

View File

@@ -243,7 +243,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
defer ls.deinit(); defer ls.deinit();
// fetch() must reject with a TypeError on network errors per spec // fetch() must reject with a TypeError on network errors per spec
ls.toLocal(self._resolver).rejectError("fetch error", .{ .type_error = @errorName(err) }); ls.toLocal(self._resolver).rejectError("fetch error", .{ .type_error = "fetch error" });
} }
fn httpShutdownCallback(ctx: *anyopaque) void { fn httpShutdownCallback(ctx: *anyopaque) void {

View File

@@ -192,8 +192,8 @@ pub fn text(self: *const Request, page: *Page) !js.Promise {
pub fn json(self: *const Request, page: *Page) !js.Promise { pub fn json(self: *const Request, page: *Page) !js.Promise {
const body = self._body orelse ""; const body = self._body orelse "";
const local = page.js.local.?; const local = page.js.local.?;
const value = local.parseJSON(body) catch |err| { const value = local.parseJSON(body) catch {
return local.rejectPromise(.{@errorName(err)}); return local.rejectPromise(.{ .syntax_error = "failed to parse" });
}; };
return local.resolvePromise(try value.persist()); return local.resolvePromise(try value.persist());
} }

View File

@@ -139,8 +139,8 @@ pub fn getText(self: *const Response, page: *Page) !js.Promise {
pub fn getJson(self: *Response, page: *Page) !js.Promise { pub fn getJson(self: *Response, page: *Page) !js.Promise {
const body = self._body orelse ""; const body = self._body orelse "";
const local = page.js.local.?; const local = page.js.local.?;
const value = local.parseJSON(body) catch |err| { const value = local.parseJSON(body) catch {
return local.rejectPromise(.{@errorName(err)}); return local.rejectPromise(.{ .syntax_error = "failed to parse" });
}; };
return local.resolvePromise(try value.persist()); return local.resolvePromise(try value.persist());
} }

View File

@@ -255,7 +255,7 @@ pub fn pipeThrough(self: *ReadableStream, transform: PipeTransform, page: *Page)
/// Returns a promise that resolves when piping is complete. /// Returns a promise that resolves when piping is complete.
pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page) !js.Promise { pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page) !js.Promise {
if (self.getLocked()) { if (self.getLocked()) {
return page.js.local.?.rejectPromise("ReadableStream is locked"); return page.js.local.?.rejectPromise(.{ .type_error = "ReadableStream is locked" });
} }
const local = page.js.local.?; const local = page.js.local.?;

View File

@@ -58,12 +58,12 @@ pub const ReadResult = struct {
pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise { pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
const stream = self._stream orelse { const stream = self._stream orelse {
return page.js.local.?.rejectPromise("Reader has been released"); return page.js.local.?.rejectPromise(.{ .type_error = "Reader has been released" });
}; };
if (stream._state == .errored) { if (stream._state == .errored) {
const err = stream._stored_error orelse "Stream errored"; //const err = stream._stored_error orelse "Stream errored";
return page.js.local.?.rejectPromise(err); return page.js.local.?.rejectPromise(.{ .type_error = "Stream errored" });
} }
if (stream._controller.dequeue()) |chunk| { if (stream._controller.dequeue()) |chunk| {
@@ -95,7 +95,7 @@ pub fn releaseLock(self: *ReadableStreamDefaultReader) void {
pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise { pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {
const stream = self._stream orelse { const stream = self._stream orelse {
return page.js.local.?.rejectPromise("Reader has been released"); return page.js.local.?.rejectPromise(.{ .type_error = "Reader has been released" });
}; };
self.releaseLock(); self.releaseLock();

View File

@@ -32,11 +32,11 @@ pub fn init(stream: *WritableStream, page: *Page) !*WritableStreamDefaultWriter
pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !js.Promise { pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !js.Promise {
const stream = self._stream orelse { const stream = self._stream orelse {
return page.js.local.?.rejectPromise("Writer has been released"); return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
}; };
if (stream._state != .writable) { if (stream._state != .writable) {
return page.js.local.?.rejectPromise("Stream is not writable"); return page.js.local.?.rejectPromise(.{ .type_error = "Stream is not writable" });
} }
try stream.writeChunk(chunk, page); try stream.writeChunk(chunk, page);
@@ -46,11 +46,11 @@ pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !
pub fn close(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise { pub fn close(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
const stream = self._stream orelse { const stream = self._stream orelse {
return page.js.local.?.rejectPromise("Writer has been released"); return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
}; };
if (stream._state != .writable) { if (stream._state != .writable) {
return page.js.local.?.rejectPromise("Stream is not writable"); return page.js.local.?.rejectPromise(.{ .type_error = "Stream is not writable" });
} }
try stream.closeStream(page); try stream.closeStream(page);
@@ -67,7 +67,7 @@ pub fn releaseLock(self: *WritableStreamDefaultWriter) void {
pub fn getClosed(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise { pub fn getClosed(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
const stream = self._stream orelse { const stream = self._stream orelse {
return page.js.local.?.rejectPromise("Writer has been released"); return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
}; };
if (stream._state == .closed) { if (stream._state == .closed) {