diff --git a/.changeset/preserve-custom-error-names.md b/.changeset/preserve-custom-error-names.md new file mode 100644 index 0000000..8b0f81e --- /dev/null +++ b/.changeset/preserve-custom-error-names.md @@ -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. diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 48008ef..82d136b 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -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; err.code = 1; diff --git a/src/serialize.ts b/src/serialize.ts index c8f7118..8874fc4 100644 --- a/src/serialize.ts +++ b/src/serialize.ts @@ -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]; }