Your page embeds `<iframe id="widget" src="https://widget.example.com">` and you call `document.getElementById('widget').postMessage(data, 'https://widget.example.com')`, which throws a TypeError. What is the correct call, and what does each argument mean?
answer
- the element is not the window
- postMessage lives on Window
- contentWindow, not contentDocument
- second argument names the receiver's origin
- no listener yet means lost message
basics
~20 sAn iframe element has no postMessage method; its window does. Call iframe.contentWindow.postMessage(data, 'https://widget.example.com') — the first argument is the copied payload, the second is the origin the frame must currently have or the browser silently drops the message.
solid answer
~40 s`postMessage` is defined on `Window`, not on `HTMLIFrameElement`, so `iframeElement.postMessage` is undefined and calling it throws. The window of a framed document is `iframeElement.contentWindow`, so the correct call is `iframe.contentWindow.postMessage(data, 'https://widget.example.com')`. The first argument is the message, copied by the structured clone algorithm rather than shared by reference. The second argument, `targetOrigin`, is a safety check the browser performs at delivery time: if the frame's current origin does not match that string, the message is discarded with no error on either side. The framed page receives it as a `message` event on its own `window`, reading the payload from `event.data`. One practical trap: `contentWindow` exists as soon as the element does, long before the framed document has registered a listener, so a message sent too early is simply lost.
code
javascript · 14 lines// Embedding page
const iframe = document.getElementById('widget');
const WIDGET_ORIGIN = 'https://widget.example.com';
window.addEventListener('message', (event) => {
if (event.origin !== WIDGET_ORIGIN) return;
if (event.data && event.data.type === 'ready') {
// Only now is a listener guaranteed to exist inside the frame.
event.source.postMessage({ type: 'setTheme', theme: 'dark' }, event.origin);
}
});
// Inside https://widget.example.com, after attaching its own listener:
// window.parent.postMessage({ type: 'ready' }, 'https://parent.example');go deeper
Be able to write the call from memory: get the element, go through contentWindow, pass the payload and the exact target origin. Say plainly that an iframe element is not a window.
Explain the delivery check the browser performs on targetOrigin, that the payload is structured-cloned rather than shared, and why a message sent before the framed page attaches its listener is simply lost.
Show the handshake you would ship: the child announces readiness, the parent captures event.source and event.origin from that first message, and both sides use a typed message envelope so the protocol can evolve.
Own the contract between embedder and widget — who publishes the message schema, how versions are negotiated at handshake time, and what a customer page is allowed to assume when your widget fails to load at all.
## The element and the window are different objects An `<iframe>` in your document is an `HTMLIFrameElement` — a node in *your* DOM tree, with attributes like `src`, `width` and `allow`. The document loaded inside it lives in a separate browsing context with its own global object. Cross-window messaging is defined on that global: the method is `Window.prototype.postMessage`. `HTMLIFrameElement` has no such method, so `iframeEl.postMessage(...)` is a plain `TypeError: ...postMessage is not a function`. Two properties bridge the element to what is inside it: - `iframeEl.contentWindow` — the framed context's `Window` (technically a `WindowProxy`). It is available **cross-origin**, but only a small set of members may be touched from another origin: `postMessage`, `closed`, `close`, `focus`, `blur`, `frames`, `length`, `top`, `parent`, `opener`, and writing `location`. - `iframeEl.contentDocument` — the framed `Document`. This is **null** whenever the frame is cross-origin, which is why reaching for it is the second most common version of this mistake. So the call is: ```js const iframe = document.getElementById('widget'); iframe.contentWindow.postMessage({ type: 'setTheme', theme: 'dark' }, 'https://widget.example.com'); ``` ## The arguments `postMessage(message, targetOrigin, transfer?)` — and there is an equivalent options form, `postMessage(message, { targetOrigin, transfer })`. `message` is not shared with the other side; it is serialized with the structured clone algorithm and a copy is delivered. Plain objects, arrays, `Date`, `Map`, `Set`, `ArrayBuffer` and typed arrays survive; functions, DOM nodes and class identity do not — attempting to send a function throws `DataCloneError`. `targetOrigin` is mandatory and is checked by the browser at delivery time. Pass the exact origin (scheme + host + port) the frame is expected to be on. If the frame is showing a different origin at that moment, the message is dropped silently — no exception for the sender, no event for the receiver. `'*'` disables the check entirely; `'/'` means "only deliver if the receiving window's origin equals mine". ## What the receiver sees The framed page listens on its own window: ```js window.addEventListener('message', (event) => { if (event.origin !== 'https://parent.example') return; console.log(event.data.type); }); ``` The event is a `MessageEvent` with `data` (the cloned payload), `origin` (the *sender's* origin, set by the browser and not forgeable by page script), `source` (a `WindowProxy` for the sender, usable to reply), and `ports` (any `MessagePort` objects that were transferred). Replying is symmetric: `event.source.postMessage(reply, event.origin)`, or from a framed page to its embedder, `window.parent.postMessage(reply, 'https://parent.example')`. ## Timing: the message is not queued for a future document `contentWindow` is non-null the instant the element is in the document, but at that point the frame may still be showing `about:blank`, and even once the real document is loading, its listener is not registered until its script runs. `postMessage` delivers to whatever document is in the frame *now*; there is no buffering for the next one. Two consequences: - Posting straight after `iframe.src = url` usually posts into `about:blank` and vanishes. - Posting on the parent's `iframe.onload` is better but still racy in practice, because load order between the frame's own scripts and the parent varies. The reliable pattern is a **child-initiated handshake**: the framed page posts `{type:'ready'}` to `window.parent` once its listener is attached, and the parent only starts sending after receiving it. That message also hands the parent a verified `event.source` and `event.origin` for replies. ## Common mistakes to name - Using `contentDocument` (null cross-origin) or the element itself instead of `contentWindow`. - Omitting `targetOrigin` — it is a required argument, so this is a `TypeError`, not a silent default. - Guessing at the origin string: `'https://widget.example.com/'` with a trailing path, or including a path segment, will not match. An origin is scheme, host and port only. - Assuming a delivered message is trustworthy on the receiving end. `targetOrigin` protects the sender's confidentiality; the receiver still has to check `event.origin` itself.
- The parent posts to the frame immediately after setting `iframe.src` and the widget never receives anything — why?`postMessage` delivers to whatever document occupies the frame at that instant, and nothing is queued for a document that has not loaded yet. Right after setting `src` the frame is still `about:blank`, or the widget's script has not attached its listener. Have the widget post a `ready` message to `window.parent` once it is listening, and start sending only after that.
- How does the framed page send a reply back to the embedder?Either `window.parent.postMessage(reply, 'https://parent.example')`, or inside the message handler `event.source.postMessage(reply, event.origin)`. The second form is preferable once you have validated `event.origin`, because it targets exactly the window that spoke to you rather than assuming the embedder is the direct parent.
- What happens if the payload contains a function or a DOM node?`postMessage` throws a `DataCloneError`. The message is serialized with the structured clone algorithm, which copies data, not code or live objects: functions, DOM nodes, `Error` subclass identity and prototype chains do not survive. Send plain data — typically a `{type, payload}` object — and reconstruct behaviour on the receiving side.
saying these in an interview costs you the question
- Thinking the iframe element itself has a postMessage method
- Reaching for contentDocument on a cross-origin frame
- Believing the message object is shared by reference
- Treating targetOrigin as optional or as a hint
- Posting right after setting src and expecting delivery