In a browser, a page calls worker.postMessage(payload) on a dedicated Web Worker and then immediately mutates payload. Does the worker see the mutation? Explain what the worker actually receives and how it reads it.
answer
- two threads, no shared objects
- the value is reconstructed, not referenced
- snapshot taken when you call it
- later edits never cross
- handler gets an event, not the payload
basics
~20 sNo. postMessage sends a copy made at the moment of the call, so later edits to the original never reach the worker. The worker gets a message event asynchronously and reads the copy from event.data; the two threads share no objects.
solid answer
~50 s`postMessage` is not a shared reference — it runs the structured clone algorithm on the value **synchronously, at the call**, and queues the resulting copy for delivery. So mutating `payload` on the next line changes only the page's object; the worker still receives what the value looked like when `postMessage` returned. On the worker side the copy arrives as a `MessageEvent` on the global scope, and you read it with `self.onmessage = (event) => { ... event.data ... }` or `self.addEventListener('message', ...)`; replying is the mirror image, `self.postMessage(result)`, read on the page as `event.data`. Delivery is asynchronous: the message becomes a task on the receiver's event loop, so nothing has happened at the instant `postMessage` returns. The practical consequence is that there is no shared mutable state between page and worker over this channel — every exchange is a copy in one direction, and whichever side owns a piece of state has to keep owning it.
code
javascript · 9 linesconst worker = new Worker('/echo.worker.js');
const payload = { count: 1, items: [] };
worker.addEventListener('message', (event) => {
console.log(event.data.count); // 1, not 2
});
worker.postMessage(payload);
payload.count = 2; // too late: the copy was taken abovego deeper
Be ready to say plainly that postMessage copies the value: the worker receives its own object, and edits made after the call never cross. Remember the payload is on event.data, not the handler argument itself.
Explain that the copy is produced synchronously at the call and delivered later as a task, that shared references and cycles inside one message survive, and that identity across separate messages does not.
Show you design around copy semantics: decide which side owns long-lived state, define an explicit message contract, and handle failure modes such as a DataCloneError on post and a messageerror on receive rather than assuming delivery.
Own the thread boundary as an API surface. Argue for payload shapes that stay cheap to copy and tolerant of version skew between page and worker code, and make state ownership explicit so the copy never becomes a hidden source of divergence.
## The only channel a worker has A dedicated Web Worker runs on its own thread with its own global scope. It cannot reach into the page's variables and the page cannot reach into its. The single sanctioned channel between them is message passing: the page holds a `Worker` object and calls `worker.postMessage(value)`; the worker calls `self.postMessage(value)` back. Each side listens for a `message` event and reads the payload from the event's `data` property. ```js // page const worker = new Worker('/echo.worker.js'); worker.onmessage = (event) => console.log(event.data); worker.postMessage({ count: 1 }); // inside echo.worker.js self.onmessage = (event) => self.postMessage(event.data.count + 1); ``` Note the shape: the handler receives an **event object**, not the payload. Reading the first argument as if it were the message itself (`worker.onmessage = (msg) => msg.count`) is the single most common beginner bug here. ## Copy, not reference When you call `postMessage`, the browser does not hand the receiving thread a pointer. It runs the *structured clone* algorithm: it walks the value you passed and builds an independent reconstruction of it for the other side. That reconstruction is what lands in `event.data`. Two things follow immediately. First, **the copy is taken at the call**, synchronously, before `postMessage` returns. Anything you do to the original afterwards is invisible to the receiver: ```js const payload = { count: 1 }; worker.postMessage(payload); payload.count = 2; // too late — the clone already recorded 1 ``` Second, **the two graphs are permanently disconnected**. The worker can mutate its copy freely; the page will never see it. There is no live binding, no proxying, no write-back. If both sides need to agree on evolving state, they have to exchange messages about it, or one side has to be the owner and the other has to ask. ## What survives inside one message Within a *single* message the clone is faithful about the shape of the graph, not just its leaves. If the same object appears twice inside the value you post, the receiver also gets it twice as one shared object; cyclic structures are reproduced as cycles rather than causing infinite recursion. That is why structured clone exists at all instead of the browser simply calling `JSON.stringify` for you. What it does **not** do is preserve identity *across* messages. Post the same object twice and the receiver gets two unrelated copies. Post a value and then post it again after mutating it, and the receiver sees two snapshots, in order. Not every value can be cloned; posting something the algorithm cannot reconstruct throws a `DataCloneError` at the `postMessage` call rather than failing silently later. Symmetrically, if a message arrives that the receiver cannot deserialize, the receiver gets a `messageerror` event instead of `message` — worth wiring up in production code, because otherwise such a failure is completely invisible. ## Delivery is a task, not a call `postMessage` returns as soon as the copy is made. The message is then queued on the receiving side and dispatched as a task on that thread's event loop. So the receiver's handler runs some time later, and — importantly — only when the receiving thread is free to run a task. If the worker is in the middle of a long synchronous computation, your message sits in its queue until that computation finishes. Message passing does not interrupt anything. Order is preserved per channel: messages posted from one side arrive in the order they were posted. There is no ordering guarantee *between* different channels, and no acknowledgement — `postMessage` is fire-and-forget, so if you need to know that the other side received or processed something, that has to be an explicit reply message you design yourself. ## Designing around it Because every exchange is a copy, the interesting design question stops being "how do I share this object" and becomes "who owns this state". The usual answer is that the worker owns the expensive thing — a parsed index, a loaded dataset, an initialized engine — and the page sends small instructions and receives small results. The moment you find yourself shipping the same large structure back and forth so both sides can "see" it, you have chosen the wrong owner, and you will pay for the copy on every message.
- How does the worker send something back, and how do you know on the page which request a reply belongs to?The worker calls `self.postMessage(value)` and the page reads it from `event.data` on the `Worker` object's `message` event. Nothing in the platform correlates a reply with a request — a single handler sees every reply. If several requests can be in flight, you have to put a correlation field in the message yourself and match replies to it.
- What happens if you post a value the browser cannot clone?`postMessage` throws a `DataCloneError` DOMException synchronously at the call site, so the message is never queued and the receiver hears nothing. That is a useful property: the failure surfaces on the thread that made the mistake, with a stack, rather than as a silently missing message on the other side.
- If the worker is busy running a long computation, what happens to a message the page posts to it?It is queued and delivered as a task once the worker's thread finishes what it is doing. Messages never interrupt running code — the worker is single-threaded internally too. This is why a "cancel" message cannot stop a synchronous loop already in progress; the worker has to check for cancellation between chunks of work, or the page has to terminate it.
saying these in an interview costs you the question
- Says page and worker share the same object
- Expects the worker to see mutations made after posting
- Thinks postMessage delivers synchronously to the other thread
- Reads the handler's argument directly instead of event.data
- Assumes a posted message interrupts code the worker is running