Skip to main content

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.

TypeWorkers RPCDO StorageCap'n WebLumenizeNotes
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 Map and Set are fully supported, object identity behavior depends on serialization boundaries:

    • Identity preserved: Within a single RPC call (Workers RPC, @lumenize/rpc) or single stringify()/parse() cycle
    • Identity lost: Across separate storage operations (DO Storage put()/get())

    See Map and Set Behavior for detailed explanation and patterns.

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, cause chains, custom properties, and subclass types (including custom Error classes!)
  • Circular References: Automatically handled with $lmz reference 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 type
  • TypeError - Type-related errors
  • RangeError - Value out of range
  • ReferenceError - Invalid references
  • SyntaxError - Syntax parsing errors
  • URIError - URI encoding/decoding errors
  • EvalError - Eval-related errors
  • AggregateError - 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]:

  1. Error is serialized with its name property (e.g., "TypeError")
  2. During deserialization, we look up globalThis["TypeError"]
  3. If found, we create a new instance of that constructor
  4. If not found, we fall back to base Error (but preserve the name property)

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: -0 becomes +0 (What is -0 anyway?)
  • Streams: ReadableStream/WritableStream not supported

Attribution​

Inspired by:

See ATTRIBUTIONS.md for full attribution details.