Skip to content

Commit 310da56

Browse files
authored
Implement custom serialization of errors as host object (#4216)
1 parent b3d66b8 commit 310da56

File tree

8 files changed

+389
-9
lines changed

8 files changed

+389
-9
lines changed

build/deps/v8.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ PATCHES = [
3232
"0024-Modify-where-to-look-for-dragonbox.patch",
3333
"0025-Disable-slow-handle-check.patch",
3434
"0026-Workaround-for-builtin-can-allocate-issue.patch",
35+
"0027-Implement-additional-Exception-construction-methods.patch",
3536
]
3637

3738
# V8 and its dependencies
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2+
From: James M Snell <[email protected]>
3+
Date: Tue, 1 Jul 2025 17:33:43 -0700
4+
Subject: Implement additional Exception construction methods
5+
6+
Signed-off-by: James M Snell <[email protected]>
7+
8+
diff --git a/include/v8-exception.h b/include/v8-exception.h
9+
index 5441a0ab6a403c566e7b0b6002e720c971480893..b9933027aaf9f7842740d6a6742a2c73da65a46a 100644
10+
--- a/include/v8-exception.h
11+
+++ b/include/v8-exception.h
12+
@@ -48,6 +48,14 @@ class V8_EXPORT Exception {
13+
static Local<Value> WasmSuspendError(Local<String> message,
14+
Local<Value> options = {});
15+
static Local<Value> Error(Local<String> message, Local<Value> options = {});
16+
+ static Local<Value> URIError(Local<String> message,
17+
+ Local<Value> options = {});
18+
+ static Local<Value> EvalError(Local<String> message,
19+
+ Local<Value> options = {});
20+
+ static Local<Value> AggregateError(Local<String> message,
21+
+ Local<Value> options = {});
22+
+ static Local<Value> SuppressedError(Local<String> message,
23+
+ Local<Value> options = {});
24+
25+
/**
26+
* Creates an error message for the given exception.
27+
diff --git a/src/api/api.cc b/src/api/api.cc
28+
index 99eb9a5dc075a0e7aa38fe31b0576a652bd12cdf..345bfb3fac0e16ad532179dd915c80b454d0cb88 100644
29+
--- a/src/api/api.cc
30+
+++ b/src/api/api.cc
31+
@@ -11281,6 +11281,10 @@ DEFINE_ERROR(WasmCompileError, wasm_compile_error)
32+
DEFINE_ERROR(WasmLinkError, wasm_link_error)
33+
DEFINE_ERROR(WasmRuntimeError, wasm_runtime_error)
34+
DEFINE_ERROR(WasmSuspendError, wasm_suspend_error)
35+
+DEFINE_ERROR(EvalError, eval_error)
36+
+DEFINE_ERROR(URIError, uri_error)
37+
+DEFINE_ERROR(AggregateError, aggregate_error)
38+
+DEFINE_ERROR(SuppressedError, suppressed_error)
39+
DEFINE_ERROR(Error, error)
40+
41+
#undef DEFINE_ERROR
42+
diff --git a/src/logging/runtime-call-stats.h b/src/logging/runtime-call-stats.h
43+
index 9509646458bd4c7b6e4cdfa7a5109a431b8121c1..03c2063841224df6b043a4edeaea9ee1f568b563 100644
44+
--- a/src/logging/runtime-call-stats.h
45+
+++ b/src/logging/runtime-call-stats.h
46+
@@ -218,7 +218,11 @@ namespace v8::internal {
47+
V(WeakMap_Delete) \
48+
V(WeakMap_Get) \
49+
V(WeakMap_New) \
50+
- V(WeakMap_Set)
51+
+ V(WeakMap_Set) \
52+
+ V(EvalError_New) \
53+
+ V(URIError_New) \
54+
+ V(AggregateError_New) \
55+
+ V(SuppressedError_New)
56+
57+
#define ADD_THREAD_SPECIFIC_COUNTER(V, Prefix, Suffix) \
58+
V(Prefix##Suffix) \

src/workerd/io/worker-interface.capnp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,11 @@ enum SerializationTag {
398398
# without breaking things).
399399

400400
abortSignal @9;
401+
402+
nativeError @10;
403+
# A JavaScript native error, such as Error, TypeError, etc. These are typically
404+
# not handled as host objects in V8 but we handle them as such in workers in
405+
# order to preserve additional information that we may attach to them.
401406
}
402407

403408
enum StreamEncoding {

src/workerd/jsg/jsvalue.c++

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ void JsObject::set(Lock& js, kj::StringPtr name, const JsValue& value) {
6969
set(js, js.strIntern(name), value);
7070
}
7171

72+
void JsObject::defineProperty(Lock& js, kj::StringPtr name, const JsValue& value) {
73+
v8::Local<v8::String> nameStr = js.strIntern(name);
74+
check(inner->DefineOwnProperty(js.v8Context(), nameStr, value));
75+
}
76+
7277
void JsObject::setReadOnly(Lock& js, kj::StringPtr name, const JsValue& value) {
7378
v8::Local<v8::String> nameStr = js.strIntern(name);
7479
check(inner->DefineOwnProperty(js.v8Context(), nameStr, value,

src/workerd/jsg/jsvalue.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ class JsObject final: public JsBase<v8::Object, JsObject> {
363363
void set(Lock& js, kj::StringPtr name, const JsValue& value);
364364
void setReadOnly(Lock& js, kj::StringPtr name, const JsValue& value);
365365
void setNonEnumerable(Lock& js, const JsSymbol& name, const JsValue& value);
366+
367+
// Like set but uses the defineProperty API instead in order to override
368+
// the default property attributes. This is useful for defining properties
369+
// that otherwise would not be normally settable, such as the name of an
370+
// error object.
371+
void defineProperty(Lock& js, kj::StringPtr name, const JsValue& value);
372+
366373
JsValue get(Lock& js, const JsValue& name) KJ_WARN_UNUSED_RESULT;
367374
JsValue get(Lock& js, kj::StringPtr name) KJ_WARN_UNUSED_RESULT;
368375

src/workerd/jsg/ser-test.c++

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,43 @@ struct SerTestContext: public ContextGlobalObject {
150150
return result;
151151
}
152152

153+
JsObject roundTripError(Lock& js, JsObject errorObj) {
154+
Serializer ser(js,
155+
Serializer::Options{
156+
.treatErrorsAsHostObjects = true,
157+
});
158+
ser.write(js, errorObj);
159+
auto content = ser.release();
160+
Deserializer deser(js, content);
161+
auto val = KJ_ASSERT_NONNULL(deser.readValue(js).tryCast<JsObject>());
162+
163+
auto names = errorObj.getPropertyNames(js, KeyCollectionFilter::OWN_ONLY,
164+
PropertyFilter::ALL_PROPERTIES, IndexFilter::SKIP_INDICES);
165+
for (size_t n = 0; n < names.size(); n++) {
166+
auto before = errorObj.get(js, names.get(js, n));
167+
auto after = val.get(js, names.get(js, n));
168+
if (before.isArray()) {
169+
auto beforeArray = KJ_ASSERT_NONNULL(before.tryCast<JsArray>());
170+
auto afterArray = KJ_ASSERT_NONNULL(after.tryCast<JsArray>());
171+
KJ_ASSERT(beforeArray.size() == afterArray.size());
172+
for (size_t i = 0; i < beforeArray.size(); i++) {
173+
KJ_ASSERT(beforeArray.get(js, i).strictEquals(afterArray.get(js, i)));
174+
}
175+
} else {
176+
KJ_ASSERT(before.strictEquals(after));
177+
}
178+
}
179+
180+
return val;
181+
}
182+
153183
JSG_RESOURCE_TYPE(SerTestContext) {
154184
JSG_NESTED_TYPE(Foo);
155185
JSG_NESTED_TYPE(Bar);
156186
JSG_NESTED_TYPE(Baz);
157187
JSG_NESTED_TYPE(Qux);
158188
JSG_METHOD(roundTrip);
189+
JSG_METHOD(roundTripError);
159190
}
160191
};
161192
JSG_DECLARE_ISOLATE_TYPE(SerTestIsolate,
@@ -284,5 +315,46 @@ KJ_TEST("serialization") {
284315
"number", "321");
285316
}
286317

318+
KJ_TEST("serialization of errors") {
319+
Evaluator<SerTestContext, SerTestIsolate> e(v8System);
320+
321+
e.expectEval(
322+
"e = new Error('a', {cause:'c'}); e.foo = true; roundTripError(e).foo", "boolean", "true");
323+
e.expectEval(
324+
"roundTripError(new TypeError('a', {cause:'c'})) instanceof TypeError", "boolean", "true");
325+
e.expectEval(
326+
"roundTripError(new RangeError('a', {cause:'c'})) instanceof RangeError", "boolean", "true");
327+
e.expectEval("roundTripError(new ReferenceError('a', {cause:'c'})) instanceof ReferenceError",
328+
"boolean", "true");
329+
e.expectEval("roundTripError(new SyntaxError('a', {cause:'c'})) instanceof SyntaxError",
330+
"boolean", "true");
331+
e.expectEval("e = new Error(); Object.defineProperty(e, 'name', {value: 'CustomError'}); "
332+
"roundTripError(e).name",
333+
"string", "CustomError");
334+
335+
// Throws due to serializing a cycle. Because we serialize the errors as
336+
// host objects, we end up being responsible for ensuring that cycles are
337+
// not present in the serialized data. For now, we're just punting on this
338+
// to keep things simple, but we end up getting an error when we try to
339+
// deserialize. Fortunately this case should always be rare.
340+
// TODO(later): Handle cycles in errors as an edge case.
341+
e.expectEval(R"(
342+
const a = new Error('a');
343+
a.cause = a;
344+
roundTripError(a);
345+
)",
346+
"throws", "Error: Unable to deserialize cloned data.");
347+
348+
e.expectEval(
349+
"roundTripError(new URIError('a', {cause:'c'})) instanceof URIError", "boolean", "true");
350+
e.expectEval(
351+
"roundTripError(new EvalError('a', {cause:'c'})) instanceof EvalError", "boolean", "true");
352+
e.expectEval("roundTripError(new AggregateError(['a', 'b'], 'c')) instanceof AggregateError",
353+
"boolean", "true");
354+
e.expectEval("roundTripError(new SuppressedError('a', 'b', 'c', {cause:'c'})) instanceof "
355+
"SuppressedError",
356+
"boolean", "true");
357+
}
358+
287359
} // namespace
288360
} // namespace workerd::jsg::test

0 commit comments

Comments
 (0)