Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/preserve-custom-error-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"capnweb": patch
---

Preserve the name of error types that can't be reconstructed on the receiving side.

Errors whose type isn't a built-in (for example a custom `ArtifactsError`) are still delivered as plain `Error`, but now keep the sender's `name`. Names that resolve to `Object.prototype` members still fall back to `Error`. This matches how workerd delivers these errors between Workers.
22 changes: 22 additions & 0 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,28 @@ describe("error serialization", () => {
expect(result.foo).toBe("bar");
});

it("preserves the name of error types it cannot reconstruct", () => {
class ArtifactsError extends Error {
name = "ArtifactsError";
code = "NOT_FOUND";
numericCode = 10200;
}

let result = roundTrip(new ArtifactsError("Repository not found: x."));
expect(result).toBeInstanceOf(Error);
expect(Object.getPrototypeOf(result)).toBe(Error.prototype);
expect(result.name).toBe("ArtifactsError");
expect(result.message).toBe("Repository not found: x.");
expect(result.code).toBe("NOT_FOUND");
expect(result.numericCode).toBe(10200);
expect(Object.keys(result)).not.toContain("name");
expect(String(result)).toBe("ArtifactsError: Repository not found: x.");

let plain = deserialize('["error","CustomError","boom"]') as Error;
expect(plain).toBeInstanceOf(Error);
expect(plain.name).toBe("CustomError");
});

it("preserves built-in subclass identity when extras are present", () => {
let err = new TypeError("t") as TypeError & Record<string, unknown>;
err.code = 1;
Expand Down
8 changes: 8 additions & 0 deletions src/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,14 @@ export class Evaluator {
// AggregateError's constructor takes (errors, message); we pass an empty array
// and patch `errors` from the props bag below.
let result = cls === AggregateError ? new cls([], value[2]) : new cls(value[2]);
// Error types we can't reconstruct (e.g. a custom `ArtifactsError`) are delivered as
// plain `Error`, but keep the sender's name so receivers can still tell them apart.
// Never adopt a name that resolves to an Object.prototype member.
if (cls === Error && value[1] !== "Error" && !(value[1] in Object.prototype)) {
Object.defineProperty(result, "name", {
value: value[1], writable: true, enumerable: false, configurable: true
});
}
if (typeof value[3] === "string") {
result.stack = value[3];
}
Expand Down
Loading