skip to content

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?

level: juniorimportance: must knowfreq 55%

answer

  1. the element is not the window
  2. postMessage lives on Window
  3. contentWindow, not contentDocument
  4. second argument names the receiver's origin
  5. no listener yet means lost message

basics

~20 s

An 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
javascript
// 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context