skip to content

What does the transfer option of structuredClone(value, { transfer }) do, and what state is the original ArrayBuffer left in afterwards?

level: middleimportance: nice to knowfreq 25%

answer

  1. move, not copy
  2. the original is left empty
  3. exactly one owner at a time
  4. views made earlier go to length zero
  5. only certain types qualify

basics

~20 s

Listing an ArrayBuffer in the transfer option moves its memory into the clone instead of copying it. The original buffer is left detached: its byteLength becomes 0 and it no longer holds any data, so nothing may read through it again.

solid answer

~50 s

By default the structured clone algorithm copies every byte, so cloning a 100 MB `ArrayBuffer` allocates another 100 MB. Passing that buffer in the `transfer` array — `structuredClone(payload, { transfer: [payload.buf] })` — moves ownership of the underlying memory to the clone instead. The result is O(1) rather than proportional to size, and the price is that the original is **detached**: `buf.byteLength` becomes `0`, any existing typed-array view over it is empty, and constructing a new view over it throws a `TypeError`. Transfer is therefore a move, not a share — there is exactly one live owner at any moment, which is what makes it safe. Only transferable objects qualify: `ArrayBuffer` is the everyday one, alongside `MessagePort`, the stream types and a few image objects. Ordinary objects, `Map`, `Set` and `Date` cannot be transferred, only copied.

code

javascript · 16 lines
javascript
const buf = new ArrayBuffer(8);
const view = new Uint8Array(buf);
view[0] = 42;

const moved = structuredClone(buf, { transfer: [buf] });

console.log(moved.byteLength);            // 8
console.log(new Uint8Array(moved)[0]);    // 42
console.log(buf.byteLength);              // 0
console.log(view.length, view[0]);        // 0 undefined

try {
  new Uint8Array(buf);
} catch (e) {
  console.log(e.constructor.name);        // "TypeError"
}

go deeper

for a junior

Know that transfer moves an ArrayBuffer's memory instead of copying it, and that the original ends up empty with a byteLength of 0.

for a middle

Explain detaching precisely — existing views go to length zero, new views throw — and know that you transfer a typed array's .buffer, never the typed array itself.

for a senior

Show when the optimisation is worth its semantics: large binary handoffs where ownership genuinely moves, and never where other code still holds views that would silently read nothing.

for a principal

Own the ownership model — which component owns a binary buffer at each stage of a pipeline, why moving beats copying at scale, and where shared memory with atomics is the honest answer instead.

## Copy versus move The structured clone algorithm's default is a copy: every reachable value is reproduced, and the original is untouched. For structured data that is what you want. For a large block of binary data it is pure waste — you pay the allocation and the memcpy for bytes you were about to hand over anyway. The `transfer` option asks for a **move** instead: ```js const buf = new ArrayBuffer(1024 * 1024); const copy = structuredClone(buf); // copies a megabyte const moved = structuredClone(buf, { transfer: [buf] }); // moves it ``` After the second call, `moved` owns the memory and `buf` owns nothing. ## What "detached" means concretely Detaching is an observable state change on the original object, not a hint: ```js const buf = new ArrayBuffer(8); const view = new Uint8Array(buf); view[0] = 42; const moved = structuredClone(buf, { transfer: [buf] }); buf.byteLength; // 0 view.length; // 0 view[0]; // undefined moved.byteLength; // 8 new Uint8Array(moved)[0]; // 42 new Uint8Array(buf); // TypeError — the buffer is detached ``` The object still exists — it is a live JavaScript object with a prototype — but it has no data block. Every view created earlier over it becomes length-zero, which is the part that catches people out: a typed array held elsewhere in the program silently stops producing data rather than throwing at the point of use. ## Why detaching is the point Single ownership is what makes the optimisation sound. If both sides kept a live handle to the same bytes, a transfer would be a shared mutable buffer across contexts, with no synchronisation of any kind. Detaching enforces that exactly one side can touch the memory afterwards, so the move cannot introduce a data race. (Deliberate sharing is a different mechanism entirely — `SharedArrayBuffer` — with its own rules and its own atomics.) ## What can be transferred Transferability is a property the platform grants to specific types, not something you can opt into. `ArrayBuffer` is the one you meet daily; `MessagePort`, `ReadableStream`, `WritableStream`, `TransformStream`, `ImageBitmap` and `OffscreenCanvas` are also transferable. Plain objects, arrays, `Map`, `Set`, `Date` and typed arrays themselves are **not** transferable — for a typed array you transfer its `.buffer`: ```js const pixels = new Uint8ClampedArray(4096); const out = structuredClone( { width: 32, pixels }, { transfer: [pixels.buffer] }, ); // out.pixels is a live Uint8ClampedArray; pixels.length is now 0 ``` Listing something non-transferable in the array throws a `DataCloneError`. ## Transfer and clone interact The `transfer` list does not replace the clone — the value is still cloned; the listed objects are merely moved rather than duplicated as part of that clone. So a payload object holding a buffer plus metadata still gets a deep copy of the metadata, with the buffer moved. And an object listed in `transfer` but not actually reachable from `value` is still detached, which is a good way to lose data by accident. ## When to reach for it Use it when the data is large, binary, and genuinely handed over: image or audio buffers, decoded file contents, results of a bulk computation. Do not use it for small payloads — detaching is a real semantic change and the copy you avoided was cheap. And never transfer a buffer that other parts of your program still hold views into, unless those parts are written to expect it; a length-zero typed array is a quiet failure, not a loud one. ## Availability The `transfer` option is part of the `structuredClone` global defined by the HTML specification; it is available wherever `structuredClone` is — evergreen browsers from early 2022 and Node 17 and later.

  • You transfer a buffer and a typed array created over it earlier is still referenced elsewhere. What does that code see?
    A length-zero view. `view.length` becomes 0 and indexed reads return `undefined` rather than throwing, so a loop over it simply does nothing. That silence is the danger: the failure surfaces as missing output somewhere downstream. Constructing a *new* view over the detached buffer does throw a TypeError, which is the louder signal.
  • Can you transfer a Uint8Array directly?
    No — typed arrays are not transferable objects. You list its underlying `.buffer` in the transfer array instead, and the clone's typed array is built over the moved memory. Passing the typed array itself throws a DataCloneError. The same is true of plain objects, Map, Set and Date: they can be cloned but never transferred.
  • How is transferring different from using a SharedArrayBuffer?
    Transfer moves ownership — exactly one side can access the bytes afterwards, so there is no concurrent access to reason about. A SharedArrayBuffer is genuinely shared memory: both sides read and write the same bytes at the same time, which requires Atomics for correctness and is gated behind cross-origin isolation requirements in browsers.

saying these in an interview costs you the question

  • Thinks both sides can still read the buffer after a transfer
  • Expects the original byteLength to stay unchanged
  • Tries to list a typed array instead of its buffer
  • Transfers small payloads where a copy costs nothing
  • Confuses transferring with sharing memory

context