The runtime exposes the WHATWG structuredClone(value, options) global. It performs a deep, structure-preserving copy of value using V8's structured clone serializer — the same one worker postMessage uses — optionally taking ownership of ArrayBuffers named in options.transfer.
const clone = structuredClone({ when: new Date(), tags: new Set(["a"]) });
const buffer = new ArrayBuffer(1024);
const moved = structuredClone(buffer, { transfer: [buffer] });
buffer.byteLength; // 0 — the memory now belongs to `moved`structuredClone(value) returns a clone of value. structuredClone(value, { transfer }) additionally transfers every ArrayBuffer in transfer.
valueis required; calling with no arguments throws aTypeError.optionsmay beundefinedornull(both mean "no transfer"); anything else must be an object, or aTypeErroris thrown.options.transferis a WebIDL sequence: any object with a callableSymbol.iteratorworks (an array, aSet, a generator). A non-iterable value — including a string primitive — throws aTypeError.
Cloneable: every primitive value except symbols — numbers (including -0, NaN and the infinities), strings, booleans, BigInt, null and undefined; plain objects and arrays; Date, RegExp, Map, Set, Error; Boolean/String/Number wrapper objects; ArrayBuffer, every typed array and DataView.
The clone preserves the shape of the graph, not just the values: an object referenced twice in the input is a single object referenced twice in the output, and cycles round-trip. Prototypes do not survive — a class instance clones to a plain object with the same own properties. Getters are invoked during cloning and their result is stored as a plain data property. Property insertion order is preserved.
SharedArrayBuffer is shared, not copied: the clone is a second SharedArrayBuffer over the same memory, so writes through either are visible through the other.
Not cloneable — each throws (see the deviations below): functions, symbols, WeakMap/WeakSet/WeakRef, Promise, and every native/interop object (Java proxies and the objects the metadata layer hands out), which have no serialized form.
Listed buffers are validated before anything is serialized: each entry must be an ArrayBuffer, must not already be detached, must be detachable, and must appear at most once. A violation throws before the source buffers are touched, so a rejected call never leaves a half-transferred graph behind.
On success the memory changes hands rather than being copied: the source buffer is detached (byteLength becomes 0, and every typed array over it becomes zero-length) and the clone receives the original backing store. A transferred buffer need not appear inside value at all; a buffer reached through a typed array in value is transferred as a unit, so the cloned view sees the original bytes.
structuredClone and worker postMessage run on the same serialization core, so everything above — which types clone, graph identity, cycles, SharedArrayBuffer sharing — holds for messages too. postMessage takes the same transfer list as a second argument:
worker.postMessage({ pixels: buffer }, [buffer]); // buffer is detached here,
// its memory now in the workerTwo differences are intentional:
- The transfer list must be an array. Omitting it, or passing
undefinedornull, means "transfer nothing"; every other non-array value is aTypeError. The WebIDL iterable-to-sequence conversion that letsstructuredClonetake aSetor any iterable lives in the JavaScript wrapper aroundstructuredClone;postMessageis native all the way down and has no such wrapper. - Host objects degrade instead of throwing. Posting a native/interop object delivers an empty object to the receiver rather than raising a
DataCloneError. This is long-standing shipped behavior, and app code relies on it;structuredClone, being new, follows the spec and rejects. The asymmetry is encoded in exactly one place — theHostObjectPolicyenum intest-app/runtime/src/main/cpp/StructuredSerialization.h— and unifying the two on rejection is a breaking change that needs the iOS runtime to move at the same time.
DataCloneErroris anError, not aDOMException. This runtime has noDOMException, so failures throw anErrorwhosenameis set to"DataCloneError". Detect failures withe.name === "DataCloneError";instanceof DOMExceptioncannot work.- Only
ArrayBufferis transferable. The spec's other transferable types —MessagePort,ImageBitmap,ReadableStreamand friends — do not exist here. A non-ArrayBufferin the transfer list is aDataCloneError. - Host objects are not cloneable by
structuredClone. The spec leaves platform objects to each host; here every native/interop wrapper is rejected with aDataCloneError, because a JavaScript copy detached from its native counterpart would be a wrapper around nothing. WorkerpostMessagedeliberately differs — see above.
SharedArrayBuffer follows the spec: it is shared rather than copied, and it is not transferable (listing one throws a DataCloneError).