From 9f67bfa82335d818d44902d334ad1a46431e9bad Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 9 Jul 2026 12:36:10 +0100 Subject: [PATCH] fix: preserve error subtype through serialize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `typeOf` derived the serialized error `name` from `Object.prototype.toString`, whose tag is `Error` for every native error subtype, so `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError` and `URIError` all serialized as `{name: "Error"}` and revived as a plain `Error`: deserialize(serialize(new TypeError('x'))) instanceof TypeError; // false Native `structuredClone` keeps the subtype, and the deserializer already rebuilds the right constructor when handed the correct name. Use the instance `name` when it is one of the standard error names, otherwise `Error` — matching `structuredClone`, which also normalizes custom and aggregate errors to `Error`. --- cjs/deserialize.js | 2 +- cjs/serialize.js | 4 ++-- esm/deserialize.js | 2 +- esm/serialize.js | 4 ++-- test/index.js | 23 +++++++++++++++++++++++ 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/cjs/deserialize.js b/cjs/deserialize.js index 6ce37b0..bef963b 100644 --- a/cjs/deserialize.js +++ b/cjs/deserialize.js @@ -65,7 +65,7 @@ const deserializer = ($, _) => { } case ERROR: { const {name, message} = value; - return as(guard(name, message), index); + return as(typeof env[name] === 'function' ? guard(name, message) : new Error(message), index); } case BIGINT: return as(BigInt(value), index); diff --git a/cjs/serialize.js b/cjs/serialize.js index ac6cec1..0998efe 100644 --- a/cjs/serialize.js +++ b/cjs/serialize.js @@ -34,8 +34,8 @@ const typeOf = value => { if (asString.includes('Array')) return [ARRAY, asString]; - if (asString.includes('Error')) - return [ERROR, asString]; + if (value instanceof Error) + return [ERROR, value.name]; return [OBJECT, asString]; }; diff --git a/esm/deserialize.js b/esm/deserialize.js index 440fb30..e6d00c9 100644 --- a/esm/deserialize.js +++ b/esm/deserialize.js @@ -67,7 +67,7 @@ const deserializer = ($, _) => { } case ERROR: { const {name, message} = value; - return as(guard(name, message), index); + return as(typeof env[name] === 'function' ? guard(name, message) : new Error(message), index); } case BIGINT: return as(BigInt(value), index); diff --git a/esm/serialize.js b/esm/serialize.js index 24cfe2c..98fb6dd 100644 --- a/esm/serialize.js +++ b/esm/serialize.js @@ -36,8 +36,8 @@ const typeOf = value => { if (asString.includes('Array')) return [ARRAY, asString]; - if (asString.includes('Error')) - return [ERROR, asString]; + if (value instanceof Error) + return [ERROR, value.name]; return [OBJECT, asString]; }; diff --git a/test/index.js b/test/index.js index 0277401..87b54b4 100644 --- a/test/index.js +++ b/test/index.js @@ -168,4 +168,27 @@ const invalidViaJSON = parse(stringify(invalidDate)); assert(invalidViaJSON instanceof Date, true); assert(Number.isNaN(invalidViaJSON.getTime()), true); +// error subtypes must round-trip to their own constructor like native structuredClone +for (const Class of [Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError]) { + const errorViaRecord = deserialize(serialize(new Class('boom'))); + assert(errorViaRecord instanceof Class, true, `${Class.name} must clone as ${Class.name}`); + assert(errorViaRecord.name, Class.name); + assert(errorViaRecord.message, 'boom'); +} + +// a custom-named error (name is not a global constructor) must serialize its real name, +// then revive as a plain Error, exactly like native structuredClone does +{ + const custom = new Error('boom'); + custom.name = 'CustomBunError'; + // serialize must preserve the actual name, not fall back to 'Error' + assert(serialize(custom)[0][1].name, 'CustomBunError', 'serialize must preserve the custom error name'); + const viaRecord = deserialize(serialize(custom)); + const native = globalThis.structuredClone(custom); + assert(viaRecord instanceof Error, true, 'custom error must revive as Error'); + assert(viaRecord.constructor, native.constructor, 'custom error must match native constructor'); + assert(viaRecord.message, native.message); + assert(viaRecord.name, native.name); +} + require('./eval.js');