Introduction
Zero-dependency serialization with full-fidelity type support for complex JavaScript types including cycles, Errors, Request/Response, special numbers, and more.
Installation
npm install @lumenize/structured-clone
Supported Types
While this package was originally optimized for the Cloudflare edge compute platform, nothing in it is Cloudflare-specific. All tests run in three environments: Node.js, headless Chromium browser, and simulated Cloudflare Workers. Our primary use case includes browser clients, and the full test suite passes in all environments.
Not a Cloudflare user? Feel free to ignore the first three columns in the comparison table below.
| Type | Workers RPC | DO Storage | Cap'n Web | Lumenize | Notes |
|---|---|---|---|---|---|
| Cycles & Aliases | ✅→❌ | ✅→❌ | ❌ | ✅ | Cap'n Web throws error Cloudflare plans to remove |
| Primitives | |||||
| undefined | ✅ | ✅ | ✅ | ✅ | |
| null | ✅ | ✅ | ✅ | ✅ | |
| Special Numbers | |||||
| NaN | ✅ | ✅ | ✅ | ✅ | |
| Infinity | ✅ | ✅ | ✅ | ✅ | |
| -Infinity | ✅ | ✅ | ✅ | ✅ | |
| Built-in Types | |||||
| BigInt | ✅ | ✅ | ✅ | ✅ | |
| Date | ✅ | ✅ | ✅ | ✅ | |
| RegExp | ✅ | ✅ | ❌ | ✅ | |
| Map | ✅⚠️ | ✅⚠️ | ❌ | ✅⚠️ | Object keys lose identity |
| Set | ✅⚠️ | ✅⚠️ | ❌ | ✅⚠️ | Object values lose identity |
| ArrayBuffer | ✅ | ✅ | ❌ | ✅ | |
| Uint8Array | ✅ | ✅ | ✅ | ✅ | |
| Errors | |||||
| Error (thrown) | ✅ | N/A | ⚠️ | ✅ | Cap'n Web loses name and remote stack |
| Error (value) | ✅ | ⚠️ | ⚠️ | ✅ | Cap'n Web loses name and remote stack |
| Web API Types | |||||
| Request | ✅ | ❌ | ❌ | ✅ | |
| Response | ✅ | ❌ | ❌ | ✅ | |
| Headers | ✅ | ✅ | ❌ | ✅ | Cap'n Web: type annotation only, no serialization |
| URL | ❌ | ❌ | ❌ | ✅ | |
| Streams | |||||
| ReadableStream | ✅ | ❌ | ❌ | ❌ | Cap'n Web: "may be added" |
| WritableStream | ✅ | ❌ | ❌ | ❌ | Lumenize: "just use WebSockets" |
Notes:
-
⚠️ Map/Set with Object Keys/Values: While
MapandSetare fully supported, object identity behavior depends on serialization boundaries:- Identity preserved: Within a single RPC call (Workers RPC,
@lumenize/rpc) or singlestringify()/parse()cycle - Identity lost: Across separate storage operations (DO Storage
put()/get())
See Map and Set Behavior for detailed explanation and patterns.
- Identity preserved: Within a single RPC call (Workers RPC,
Key Features
- Platform Independent: Works in Node.js, browsers, Deno, Bun, and Cloudflare Workers
- Full Type Support: All primitives, special numbers (
NaN,Infinity,-Infinity), built-in types (Date,RegExp,Map,Set), typed arrays, and Web API objects (via RequestSync/ResponseSync) - Error Fidelity: Preserves stack traces,
causechains, custom properties, and subclass types (including custom Error classes!) - Circular References: Automatically handled with
$lmzreference markers - Zero Dependencies: No external packages required
- Synchronous API: No async/await needed for serialization
Basic Usage
The core API is simple: stringify() to serialize, parse() to deserialize.
Both functions are synchronous - no await needed!
import { stringify, parse } from '@lumenize/structured-clone';
const user = {
name: 'Alice',
age: 30,
active: true
};
const serialized = stringify(user);
const restored = parse(serialized);
expect(restored).toEqual(user);
This works with complex nested structures, arrays, and all types shown in the table above.
Key Features with Examples
Error Chains with cause
Error chaining is fully preserved, including nested causes:
const networkError = new Error('Connection timeout');
const appError = new Error('Failed to fetch user data', {
cause: networkError
});
const restored = parse(stringify(appError));
expect(restored.message).toBe('Failed to fetch user data');
expect(restored.cause).toBeInstanceOf(Error);
expect(restored.cause.message).toBe('Connection timeout');
Custom Error Properties
Add arbitrary properties to Errors—they're all preserved:
const apiError: any = new Error('API request failed');
apiError.statusCode = 500;
apiError.endpoint = '/api/users';
const restored: any = parse(stringify(apiError));
expect(restored.message).toBe('API request failed');
expect(restored.statusCode).toBe(500);
Error Subclass Preservation
All standard JavaScript Error types are automatically preserved with correct instanceof behavior:
Standard Error Types (Built-in):
Error- Base error typeTypeError- Type-related errorsRangeError- Value out of rangeReferenceError- Invalid referencesSyntaxError- Syntax parsing errorsURIError- URI encoding/decoding errorsEvalError- Eval-related errorsAggregateError- Multiple errors combined
Example:
const typeError = new TypeError('Expected string, got number');
const restored = parse(stringify(typeError));
expect(restored).toBeInstanceOf(TypeError); // ✅ Type preserved!
expect(restored).toBeInstanceOf(Error); // ✅ Also an Error
expect(restored.name).toBe('TypeError');
expect(restored.message).toBe('Expected string, got number');
How it works:
We use dynamic constructor lookup via globalThis[error.name]:
- Error is serialized with its
nameproperty (e.g.,"TypeError") - During deserialization, we look up
globalThis["TypeError"] - If found, we create a new instance of that constructor
- If not found, we fall back to base
Error(but preserve thenameproperty)
Custom Error Classes:
Custom Error classes work too, but require explicit global registration since module imports are not in global scope:
// Your custom Error class (in a module)
export class ValidationError extends Error {
name = 'ValidationError';
constructor(message: string, public field: string) {
super(message);
}
}
To enable type preservation, register it globally:
// Register on globalThis so deserializer can find it
(globalThis as any).ValidationError = ValidationError;
Now serialization preserves the type:
const error = new ValidationError('Invalid email', 'email');
const restored = parse(stringify(error));
// ...
expect(restored instanceof ValidationError).toBe(true); // ✅ true!
expect((restored as any).field).toBe('email'); // ✅ 'email' (custom property preserved)
expect(restored.name).toBe('ValidationError'); // ✅ 'ValidationError'
Without global registration:
If you don't register the class globally, deserialization falls back to base Error but still preserves custom properties and the name:
Web API Objects Example
Serialize RequestSync/ResponseSync objects for storage, queues, or RPC:
import { RequestSync, ResponseSync } from '@lumenize/structured-clone';
const apiData = {
incomingRequest: new RequestSync('https://api.example.com/endpoint', {
method: 'POST',
body: 'request data'
}),
outgoingResponse: new ResponseSync('response data', {
status: 200
}),
timestamp: Date.now()
};
const restored = parse(stringify(apiData));
expect(restored.incomingRequest).toBeInstanceOf(RequestSync);
// ...
Note: Use RequestSync/ResponseSync instead of native Request/Response for synchronous body access and serialization support.
Advanced API
The package provides a three-tier API for different use cases:
Tier 1: stringify() / parse() - JSON String I/O
The main API for most use cases. Serializes to/from JSON strings:
const jsonString = stringify(complexObject); // Returns JSON string
const restored = parse(jsonString); // Reconstructs from JSON string
Tier 2: preprocess() / postprocess() - Intermediate Format
For scenarios where you want a simple object format without stringification:
const intermediate = preprocess(complexObject); // Returns { root, objects }
const restored = postprocess(intermediate); // Reconstructs from object
Use for: MessagePort, BroadcastChannel, BJSON, Cloudflare: Workers RPC, Durable Object KV Storage, Queues, etc.
Performance
Binary formats are generally smaller, but this implementation is generally faster at serialization for similar type support. We're particularly proud of our optimized one-pass algorithm for alias and cycle detection.
Limitations
- Symbols: Cannot be serialized (throws
TypeError) - Functions: Converted to internal markers (preserved structure, not executable)
- Signed Zero:
-0becomes+0(What is -0 anyway?) - Streams:
ReadableStream/WritableStreamnot supported
Attribution
Inspired by:
- Cap'n Web (tuple format) - https://github.com/cloudflare/capnweb
- @ungap/structured-clone (cycle detection) - https://github.com/ungap/structured-clone by Andrea Giammarchi
See ATTRIBUTIONS.md for full attribution details.