Keep port1. Move port2. The original handle dies. The twin lives in the other context.
A MessagePort is transferable, not cloneable. You move it in a transfer list on a window (or worker) postMessage.
The payload of that hop can be a dummy string. The port arrives as event.ports[0], a new object in the receiver’s realm, still entangled with the end you kept.
(HTML §9.4.1,
§9.3.3,
MDN: Using channel messaging)
const { port1, port2 } = new MessageChannel();
port1.onmessage = onMsg;
iframe.contentWindow.postMessage("init", origin, [port2]);
// iframe
const port = event.ports[0];
port.onmessage = onMsg; // start() implied
Transfer is irreversible. The sender’s port2 is detached (neutered): it is not a live twin anymore, and transferring it again throws
DataCloneError. Putting the port in the message data instead of the transfer list is the same throw — structured clone has no serializable copy of a port.
The received port’s queue is born disabled; onmessage still implies start().
(HTML §2.7.2,
§9.4.4 transfer steps)
A real MessageChannel. Parent keeps port1. Transfer ships port2 into a real iframe document.
Then send on the port, try a clone, try moving the same handle again.
The lab needs JavaScript in this page.
Read the three-line handoff in
HTML Living Standard §9.4.1
(otherWindow.postMessage('hello', 'https://example.com', [channel.port2])).
Then the receiver side in
MDN: Using channel messaging
(event.ports[0]).
If clone-vs-move is still mushy — especially “why isn’t the port just event.data?” — ask.
Next: lesson 4 — when a dedicated worker’s implicit port is enough, and a short playbook for choosing or rejecting a pair.
Pocket cards:
entangled ports,
port API,
transfer.