completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Messaging & structured cloning

Send data safely between windows, workers, iframes, and tabs with structuredClone, MessageChannel, BroadcastChannel, and transfer lists.

You will learn to
  • 01
    Copy more kinds of dataUse structuredClone to copy dates, maps, sets, buffers, errors, and objects that point to themselves.
  • 02
    Pick a way to send messagesUse MessageChannel for a private line between two places, and BroadcastChannel to tell every listener on your origin.
  • 03
    Hand over big dataExplain how a transfer list hands an ArrayBuffer over, why the sender's copy becomes empty, and why that can be faster than copying.

Sending data to another place

A web page often has to send data to another place. For example:

  • A page sends data to an iframe: a page shown inside your page.
  • A page sends work to a worker: a script that runs in the background.
  • A settings tab tells your other open tabs that the theme changed.

These places can't share one object. So the browser sends them a copy of the data. For some data, it can hand the data over instead.

The browser copies the data with a set of rules called the structured clone algorithm. "Clone" means copy. The same rules are used by:

  • structuredClone(), which copies a value inside one script.
  • postMessage(), which sends a message to a window, an iframe, or a worker.
  • MessagePort and BroadcastChannel, two more ways to send messages.
  • IndexedDB, the browser's database, when it saves a value.

These rules copy many kinds of JavaScript values. They can even copy an object that points to itself. For some binary data, they can also transfer it: hand it over instead of copying it.

When you send a message to another window, say who may receive it. The second argument of postMessage is the target origin. An origin is the start of a web address: the scheme, host, and port, like https://widget.example. The browser delivers the message only if the receiving window has that origin. Never use "*", which means "any origin", for a secret message.

When you receive a message, check two things before you trust event.data:

  • event.origin: the origin of the page that sent it.
  • event.source: the window that sent it.

The code below is a small model of those two checks.

Accept messages only from the window you trustPop out in the code editor (opens in a new tab)JavaScript
const trustedOrigin = "https://widget.example";const trustedWindow = "widget-window";const event = { origin: trustedOrigin, source: trustedWindow, data: { type: "ready" } }; function receive(event) {  if (event.origin !== trustedOrigin || event.source !== trustedWindow) return "ignored";  return event.data.type;} console.log(receive(event));console.log(receive({ ...event, origin: "https://evil.example" }));

Line 6 returns "ignored" if the origin or the window is wrong. Line 10 passes the trusted message, so it prints ready. Line 11 sends the same message from https://evil.example, so it prints ignored.

The model uses plain strings in place of real windows, so it gives the same result every time. In a real page, use the iframe or popup window you created in place of trustedWindow. When you send, pass the same trusted origin as the second argument of postMessage.

Real-life analogyCopy or transfer: a photocopy or the original form

You need to send a form to another office. If you mail a photocopy, you keep the original. If you mail the original, the other office has it and you don't. Messages in JavaScript make the same choice. Most data is copied. Some special objects, such as an ArrayBuffer (a block of raw bytes), can be transferred instead.

In real life: Mailing a photocopy
In JavaScript: Copying: both sides have their own data
In real life: Mailing the original
In JavaScript: Transferring: the receiver gets the data, and the sender can't use it any more
In real life: A thick binder takes a long time to copy
In JavaScript: A big ArrayBuffer takes a long time to copy
In real life: The courier refuses some things
In JavaScript: Some values can't be copied, and you get a DataCloneError

The Windows, iframes & postMessage lesson showed how to send a message. This lesson is about the data inside the message: what can be copied, what can't, and what can be handed over.

structuredClone copies more kinds of data than JSON

INTERACTIVE

structuredClone(value) makes a deep copy of a value. A deep copy copies the object and everything inside it, so the copy shares nothing with the original. It uses the same rules as messages.

You may know JSON.stringify and JSON.parse from the JSON lesson. JSON turns data into text and back, and many types get lost on the way. structuredClone keeps them. It can copy:

  • Date, Map, Set, and RegExp objects.
  • ArrayBuffer objects and typed arrays such as Uint8Array.
  • Blob and File objects.
  • Common Error objects.
  • Objects that point to themselves. These are called circular references.

It copies data, not everything. It does not keep:

  • The methods of your own classes. The copy doesn't get the class's prototype, the object where those methods live.
  • Getters. A getter is a function that runs when you read a property.
  • A property's settings, such as whether it can be changed.

It reads the object's own properties, saves their current values, and builds new objects from them.

Real-life analogystructuredClone is a careful moving company

Picture movers who list every item in your flat, pack it, and set up matching furniture in a new flat. The new flat looks the same, but it is separate. If you move a chair in the new flat, the old flat doesn't change. In the same way, changing a copied array doesn't change the original array. And an object made from your class arrives as plain data, without its methods.

In real life: Pack furniture, dishes, and boxes
In JavaScript: Copy dates, maps, sets, arrays, buffers, and nested objects
In real life: Set up the furniture in the new flat
In JavaScript: Build new objects, not the same objects as before
In real life: Refuse things that can't be packed
In JavaScript: Throw an error for functions, DOM nodes, symbols, WeakMap, and WeakSet
In real life: Leave the brand label behind
In JavaScript: Your class's prototype and methods don't come along
Changing the copy leaves the original alonePop out in the code editor (opens in a new tab)JavaScript
const cart = { items: ["tea"] };const copy = structuredClone(cart);copy.items.push("bread");console.log(cart.items.join(","));console.log(copy.items.join(","));

Line 1 makes a cart with a list of items. Line 2 copies the cart. The copy gets its own new list. Line 3 adds bread to the copy's list only. So line 4 prints tea, and line 5 prints tea,bread.

Left: a cart called cart holds one item, tea. An arrow labeled structuredClone points to a second cart called copy, which holds tea and bread after line 3. The first cart prints tea, the copy prints tea,bread.teacartprints teastructuredCloneteabreadcopyprints tea,bread
structuredClone builds a brand-new cart with its own list inside. Line 3 adds bread to the copy only, so the first cart still holds just tea.
A copied Map and Set still workPop out in the code editor (opens in a new tab)JavaScript
const prices = new Map([["tea", 3]]);const tags = new Set(["hot"]);const copy = structuredClone({ prices, tags });console.log(copy.prices.get("tea"));console.log(copy.tags.has("hot"));

Line 3 copies an object that holds a Map and a Set. The copies are still a real Map and a real Set. So line 4 prints 3, and line 5 prints true. JSON would turn both of them into empty objects, {}.

Clone inspector
What the inspector doesPop out in the code editor (opens in a new tab)JavaScript
const original = makeValue(choice);const copy = structuredClone(original);inspect(copy);
Resultwaiting

Choose a value and press Inspect clone.

Try it yourself

Pick a value, read the code, then press Inspect clone. Your browser's own structuredClone makes the result.

Each result is worked out in your browser when you press the button.

Try the "Class instance" choice. A class instance is an object made with new from your own class. Its fields are copied, but its method is lost, because the prototype isn't copied.

Then try "Object with a getter". The getter runs once, during the copy. The copy stores the number it returned as a normal value. The getter itself is gone.

What can't be copied, and what changes

SORT

A simple rule: data can be copied, but behavior and live browser objects usually can't. Here is why each of these fails:

  • A function is behavior. It also remembers the variables around it.
  • A DOM node, such as a button, belongs to one live page.
  • A symbol is a unique value. A copy would be a different value.
  • A WeakMap or WeakSet can lose entries at any time, when the browser cleans up objects nobody uses. So its contents can't be listed and copied.

For these values, structuredClone throws a DataCloneError. It doesn't pretend it copied them.

Can it be copied?
  • Date
  • Map with object keys
  • Set
  • Object that points to itself
  • RegExp with lastIndex = 5
  • Object with a getter
  • Object made from your own class
  • Hidden (non-enumerable) property
  • Function
  • DOM node
  • Symbol value
  • WeakMap
Try it yourself
0 of 12 correct

Sort each value by what structuredClone does with it. Watch for values that copy but come out a little different.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
A function can't be copiedPop out in the code editor (opens in a new tab)JavaScript
const order = { onDone: () => "saved" };structuredClone(order);

The function on line 1 is behavior, not data. So line 2 throws a DataCloneError. The fix is to send plain data that names the action, such as { type: "save" }. The receiver then runs its own function for that action.

Left: a cart called order holds a function card, onDone. Cloning it fails with DataCloneError. Right: a cart called message holds a plain card, type: "save". Cloning it works.a functiononDone()orderDataCloneErrorplain datatype: "save"messagecopies fine
A function is a piece of behavior, so structuredClone refuses it. Plain data, such as type: "save", copies without trouble.
Only the object's own, visible properties are copied

For plain objects and objects made from your own classes, the copy takes only the object's own enumerable properties. Enumerable means the property shows up in Object.keys(). So:

  • Properties that are not enumerable are left out.
  • A getter becomes a plain value.
  • Property settings, such as writable and configurable, are not kept.

The Enumerability & ownership and Property flags & descriptors lessons explain these words in more detail.

One more change to know about. A RegExp (a regular expression) is copied, but its lastIndex is set back to 0. In a global search, lastIndex is where the next search starts. So the copy starts searching from the beginning again.

MessageChannel: a private line

INTERACTIVE

new MessageChannel() makes two linked ports, port1 and port2. A port is one end of the line. Both ports are MessagePort objects. A message posted on one port arrives at the other.

You keep one port and transfer the other to a worker or an iframe. Now the two sides have a private line. They don't need the shared message listener on window any more.

Real-life analogyMessageChannel is a pair of walkie-talkies

A normal window message is like calling out across a busy hall. Anyone who is listening may hear it. A MessageChannel is like handing a friend one of a pair of walkie-talkies. Only the two of you can talk on it.

In real life: Two walkie-talkies set to talk to each other
In JavaScript: port1 and port2 from one MessageChannel
In real life: You give one to a friend
In JavaScript: You transfer a port to an iframe or a worker
In real life: You speak into one, and your friend hears it
In JavaScript: postMessage on one port fires a message event on the other
In real life: You switch the walkie-talkie off
In JavaScript: port.close() closes the line
MessageChannel lab: a private line
Set up the private linePop out in the code editor (opens in a new tab)JavaScript
const channel = new MessageChannel();channel.port1.onmessage = (event) => log("port1", event.data);channel.port1.start();iframe.contentWindow.postMessage("connect", "*", [channel.port2]);channel.port1.postMessage("hello iframe");
Private logport1 ↔ iframe
  • Press Connect iframe to transfer port2.
Try it yourself

new MessageChannel() makes two ports. This lab transfers port2 to an iframe, then uses port1 to talk to it in private. The parent listens with addEventListener and start(). The iframe uses onmessage.

The iframe is sandboxed: it may only run scripts. It takes the port only if the setup message came from its parent page.

The lab listens for messages in two ways. You'll see both in real code:

  • The parent page uses addEventListener("message", ...). With this way, you must call port.start() before messages arrive.
  • The iframe sets port.onmessage = .... Setting onmessage starts the port for you.

Calling start() when you don't need to does no harm, and it makes the code easier to read.

BroadcastChannel: a message for everyone

INTERACTIVE

BroadcastChannel is for announcements to everyone, not private talks. You make one with a name, like new BroadcastChannel("updates"). Every channel with the same name on the same origin hears messages from the others. That includes channels in other tabs, windows, iframes, and workers.

The channel object that sends a message does not get its own message.

Real-life analogyBroadcastChannel is a building intercom

Picture the intercom in a block of flats. One announcement reaches everyone listening on that channel in the building. In the same way, BroadcastChannel works well for messages like "settings changed", "log out everywhere", or "cache updated".

In real life: Everyone tuned to channel 3 hears the announcement
In JavaScript: Every BroadcastChannel with the same name and origin gets the message
In real life: The speaker already knows what they said
In JavaScript: The channel that sends doesn't get its own message
In real life: Only people in this building hear it
In JavaScript: A site with a different origin can't hear your channel
BroadcastChannel lab: an intercom for one origin
Three listeners, one channel namePop out in the code editor (opens in a new tab)JavaScript
const a = new BroadcastChannel("lesson-room");const b = new BroadcastChannel("lesson-room");const c = new BroadcastChannel("lesson-room");b.onmessage = (event) => log("B heard " + event.data);c.onmessage = (event) => log("C heard " + event.data);a.postMessage("standup at 10");
Intercom logsame origin
  • Press Create channels, then broadcast from A, B, or C.
Try it yourself

Every BroadcastChannel with the same name and origin hears messages from the others. The channel that sends doesn't get its own message. Open this lesson in a second tab to see another tab hear it too.

Reset closes the channels so this page stops listening.
Left, MessageChannel: the parent page holds port1 and the iframe holds port2, with arrows going both ways between them. It is a private line. Right, BroadcastChannel: window A posts a message, and arrows go to windows B and C, which both hear it. A does not hear its own message.MessageChannelparentport1iframeport2private lineBroadcastChannelApostsBhearsChearsA doesn't hear itself
A MessageChannel joins exactly two ends. A BroadcastChannel message goes to every other channel with the same name, but not back to the sender.

Open this lesson in a second tab and press Create channels there too. The second tab is on the same origin, so its channels hear the messages from this tab as well.

Transferring or copying

INTERACTIVE

Copying makes separate data on both sides. Transferring hands an object over: after a transfer, only the receiver can use it.

Only some objects can be transferred. They are called transferable objects. The clearest example is an ArrayBuffer, a block of raw bytes. After a transfer, the sender's buffer is detached: it is empty and can't be used. Its byteLength, the size in bytes, becomes 0. The receiver now has the bytes.

Transfer lab: copy or move the bytes
Copy and transferPop out in the code editor (opens in a new tab)JavaScript
const buffer = new ArrayBuffer(8);const copy = structuredClone(buffer);console.log(buffer.byteLength, copy.byteLength); const moved = structuredClone(buffer, { transfer: [buffer] });console.log(buffer.byteLength, moved.byteLength);
Buffer logreal byteLength
  • Press a button to copy or transfer a buffer.
Try it yourself

A copy leaves the original usable. A transfer hands the bytes over: the sender's ArrayBuffer is detached, so its byteLength becomes 0. Times change from one browser and computer to another, so compare the sizes first.

Use a transfer for big binary data when the sender doesn't need the bytes any more.
Top row, copy: the box buffer holds 8 bytes, and the box copy also holds 8 bytes. Line 3 logs 8 8. Bottom row, transfer: the box buffer now holds 0 bytes, and the box moved holds the 8 bytes. Line 6 logs 0 8.8 bytesbuffercopy8 bytescopyline 3 logs8 80 bytesbuffertransfer8 bytesmovedline 6 logs0 8
A copy leaves both boxes with 8 bytes. A transfer moves the bytes into moved, so buffer is left with 0.

A transfer list is an array of the objects to hand over instead of copy. You'll use it in two places:

  • structuredClone(value, { transfer: [buffer] })
  • port.postMessage(value, [buffer])

Use a transfer list for big images, sound clips, or other binary data that the sender doesn't need any more.

The table compares the three tools in this lesson. Each row asks one question. Each column gives the answer for one tool.

How structuredClone, MessageChannel, and BroadcastChannel compare
What you want to knowstructuredCloneMessageChannelBroadcastChannel
What is it best for?Making a deep copy of data inside one script.Sending messages between exactly two places.Telling every listener on the same origin.
How are the two sides joined?They aren't. It just returns a copy.By two linked MessagePort objects.By the same channel name on the same origin.
Does the sender get its own message?It doesn't send messages.Only if the other port sends one back.No. The channel that posts doesn't get its own message.
Can it take a transfer list?Yes: structuredClone(value, { transfer }).Yes: port.postMessage(value, transfer).No. Its postMessage only copies.

When the message arrives

STEP THROUGH

postMessage returns right away. The receiver's message event runs later, after the current code finishes. Posting a message puts its delivery in a queue. It doesn't stop your code and jump to the receiver. The Event loop lesson explains this queue.

Step through: which line runs first?
Step 0 of 6Ready
Your turn: follow the blue line

Step through the order of postMessage. Pick copy or transfer, then predict which log comes first and how many bytes the sender keeps.

Running in
  1. script
Next: line 1
const buffer = new ArrayBuffer(4); port1.onmessage = (event) => {  console.log("message event:", event.data.byteLength);}; port2.postMessage(buffer);console.log("after postMessage:", buffer.byteLength);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
How should line 8 send the buffer?

Changing this starts a new run. Predict what line 9 logs before you step.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The setting changes line 8. In copy mode, the sender still has its buffer, so line 9 logs 4. In transfer mode, line 8 takes the buffer away at once, so line 9 logs 0. In both modes, the receiver's event runs last and sees all 4 bytes.

Where you will use this

PRACTICAL

You'll use these tools when a web app splits its work across several places. For example:

  • A canvas sends its pixels to a worker to be changed.
  • An iframe gets a private port to reply on.
  • A settings page tells your other tabs to refresh.
  • A worker sends back data it has read, so the page doesn't freeze while it waits.
Send pixels to a worker without copying themJavaScript
const worker = new Worker("image-worker.js", { type: "module" });const pixels = canvasBuffer; worker.postMessage(  { job: "blur", width, height, pixels },  [pixels],); worker.onmessage = (event) => {  drawPixels(event.data.pixels);};

Line 1 starts a worker. Lines 4–7 send it a job:

  • Line 5 is the message. Small details such as width and height are copied.
  • Line 6 is the transfer list. The big pixels buffer is handed over, not copied.

After sending, the page must not read pixels, because the worker now owns it. When the worker is done, it can transfer a new buffer back. Line 9 handles that reply.

Which tool should you use?
  • Need a deep copy inside one script? Use structuredClone.
  • Need a private line with exactly two ends? Use MessageChannel.
  • Need to tell every tab or worker on your origin? Use BroadcastChannel.
  • Need to send big binary data that you won't use again? Add a transfer list.

Common misconceptions

"structuredClone is just JSON.parse(JSON.stringify(...)), but nicer."

Not true. JSON turns data into text. On the way it loses dates, maps, sets, and properties set to undefined, and it throws on objects that point to themselves. structuredClone copies all of these, and it never makes text.

"A deep copy keeps my class's methods."

Not true. Built-in types such as Date and Map stay the same type. Your own class does not. The copy gets the object's own enumerable fields only. It doesn't get the methods or the private fields (the ones that start with #).

"postMessage runs the receiver straight away."

Not true. postMessage returns before the receiver's message event runs. That's why the order experiment logs the line after postMessage first.

"BroadcastChannel reaches every website."

Not true. It only reaches the same origin. A channel named "updates" on your site is separate from a channel with the same name on another site.

"Transfer is just a faster copy."

Not true. A transfer hands the data over. The sender's ArrayBuffer becomes detached, with a size of 0. Code that still tries to read it will break.

"If an object can hold it, it can be copied."

Not true. Objects can hold functions, DOM nodes, symbols, and WeakMaps. structuredClone refuses all of these. They are behavior, live parts of a page, unique values, or collections that can lose entries.

Practice: copy it, send it, hand it over

5 EXERCISES
Exercise 1 · Warm-upPredict a copy

Predict both lines. Then run the code in your browser console or in Node 22.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const original = { items: ["a"], when: new Date("2024-01-01T00:00:00.000Z") };
const copy = structuredClone(original);
copy.items.push("b");
console.log(original.items.join(""));
console.log(copy.when instanceof Date);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeTransfer a buffer

    What sizes print after you transfer an ArrayBuffer? Its byteLength is its size in bytes.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const buffer = new ArrayBuffer(16);
    const moved = structuredClone(buffer, { transfer: [buffer] });
    console.log(buffer.byteLength);
    console.log(moved.byteLength);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the bug: sending a function

      A teammate writes worker.postMessage({ done: () => alert("ok") }). What error name will they get?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeGetter or value?

        Predict what happens to a getter when you copy an object. Object.getOwnPropertyDescriptor(copy, "answer") returns the property's settings, including its get function if it has one.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const source = { get answer() { return 42; } };
        const copy = structuredClone(source);
        console.log(copy.answer);
        console.log(typeof Object.getOwnPropertyDescriptor(copy, "answer").get);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePick the tool for a private line

          You have an iframe. After a first hello, you want a private line just for your page and the iframe. Which tool makes that line?

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            8 QUESTIONS

            Each choice explains why it is right or wrong. Read the wrong ones too: they show the most common mistakes.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which sentence best describes structuredClone?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this Date copy print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const copy = structuredClone(new Date("2024-01-01T00:00:00.000Z"));
              console.log(copy instanceof Date);
              console.log(copy.toISOString());

              Choose an answer to see the explanation.

            3. Question 3 of 8What happens to a RegExp's lastIndex?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const re = /go/g;
              re.lastIndex = 2;
              const copy = structuredClone(re);
              console.log(copy.source);
              console.log(copy.lastIndex);

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this getter copy print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const original = { get score() { return 9; } };
              const copy = structuredClone(original);
              console.log(copy.score);
              console.log(typeof Object.getOwnPropertyDescriptor(copy, "score").get);

              Choose an answer to see the explanation.

            5. Question 5 of 8Which tool is the pair of walkie-talkies?

              Choose an answer to see the explanation.

            6. Question 6 of 8What does a transfer do to the sender's ArrayBuffer?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const buffer = new ArrayBuffer(8);
              const moved = structuredClone(buffer, { transfer: [buffer] });
              console.log(buffer.byteLength);
              console.log(moved.byteLength);

              Choose an answer to see the explanation.

            7. Question 7 of 8Channels A, B, and C share one name. Who gets a message that A posts?

              Choose an answer to see the explanation.

            8. Question 8 of 8What does this transfer code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const buffer = new ArrayBuffer(4);
              structuredClone(buffer, { transfer: [buffer] });
              console.log("after transfer:", buffer.byteLength);

              Choose an answer to see the explanation.

            Key takeaways

            • structuredClone makes a deep copy. It keeps many types that JSON loses: Date, Map, Set, typed arrays, Error objects, and objects that point to themselves.
            • It refuses behavior and live objects: functions, DOM nodes, symbols, WeakMap, and WeakSet.
            • An object made from your own class arrives as plain data. Its methods, getters, and property settings are not kept.
            • MessageChannel gives you two private ports. BroadcastChannel sends to every other channel with the same name and origin.
            • A transfer list hands data over. After a transfer, the sender's ArrayBuffer has a byteLength of 0.

            Remember the one-liner.
            Structured cloning is how the browser copies data for structuredClone and for messages; a transfer list hands data over instead of copying it.

            Up next: Web Components. You'll build your own HTML elements with custom elements, Shadow DOM, templates, and slots.

            CompleteFrontend Clear concepts. Working examples.