Why is document.write() treated as unsafe on a modern web page — what happens when it runs after the document has finished parsing, and what does it do to loading when it is used to inject a <script src> tag?
answer
- it writes into the parser's input stream
- timing decides everything
- after load, it opens a fresh document
- late discovery plus a blocking fetch
- one browser refuses to honour it
basics
~20 sCalled during parsing it injects markup into the token stream, but called after the document has loaded it implicitly reopens the document and wipes everything already there. Injecting a script tag with it also forces a parser-blocking fetch on the critical path.
solid answer
~50 s`document.write()` writes into the parser's input stream at the position of the running script, which only makes sense while the parser is still active on the original document. Once parsing has finished — from a timer, an event handler, or any code that runs after load — the call implicitly performs `document.open()`, which clears the existing document: every node, listener, and piece of application state is thrown away and replaced by whatever you wrote. From a `defer`, `async`, or dynamically inserted script, the call is either destructive in that way or simply dropped, and neither outcome is what the author intended. Using it to inject `<script src="...">` is worse still, because the injected script is parser-blocking and is discovered as late as possible; Chrome ships an intervention that blocks such cross-origin injected scripts on slow connections. The replacements are DOM APIs or `insertAdjacentHTML`.
code
javascript · 7 lines// Destructive: no insertion point exists after parsing finished
setTimeout(() => document.write('<p>replaces the whole page</p>'), 1000);
// Additive replacement
setTimeout(() => {
document.body.insertAdjacentHTML('beforeend', '<p>appended safely</p>');
}, 1000);go deeper
Know that document.write is not a way to add content to a page and that calling it after the page has loaded erases what is already there. Use DOM methods or insertAdjacentHTML instead.
Explain the insertion point: the call writes into the parser's input stream while parsing, and implicitly reopens the document otherwise. Connect it to why classic scripts must block the parser.
Be ready to trace a mysterious blank page back to a late write, and to describe how you would replace or sandbox a vendor snippet that uses it, including what the browser intervention does on slow connections.
Own the third-party script policy: which vendors may inject markup at all, whether their tags are iframed, and how you keep a blocking, document.write-based snippet from being pasted into a shared template.
## What the API was designed to do `document.write()` inserts a string into the HTML parser's **input stream**, at the insertion point — the position of the script that is currently running. Used from an inline classic script during parsing, it behaves like markup that had been in the file all along: ```html <script>document.write('<p>generated inline</p>');</script> ``` The parser reads the injected text next, tokenises it, and builds nodes from it. This is the whole reason classic scripts block the parser: because a script can rewrite the bytes about to be consumed, the parser cannot safely run ahead. ## The destructive case The insertion point only exists while the parser is running the script. Once the document has finished parsing, there is no insertion point, and the call takes a very different path: it implicitly runs `document.open()`, which **clears the document** before writing. Everything goes — the DOM tree, every event listener attached to it, every reference your code held to a node, and any state tied to those nodes. ```js setTimeout(() => { document.write('oops'); // the page is now just this text }, 1000); ``` The page does not append text; it becomes that text. Callers hit this from a click handler, an XHR callback, or an ad tag that arrived late, and the symptom — a blank page with a fragment of content — looks nothing like a scripting error. From a `defer`, `async`, or script-inserted script the outcome is bad in a different way: the write is either dropped entirely or destroys the document, depending on whether parsing has already finished. Either way, `document.write` from anything other than a parser-blocking inline script is a bug. ## Injecting scripts with it The historically common use was third-party tag injection: ```html <script> document.write('<script src="https://ads.example.com/tag.js"><\/script>'); </script> ``` This is the worst possible loading pattern. The outer script must be fetched and executed before the inner URL is even known, so discovery is maximally late; the injected script is parser-blocking, so the page stalls again for a second round trip; and it is typically cross-origin, so the stall is at the mercy of someone else's server. On a slow connection the combination can add seconds before anything renders. Browsers pushed back on this directly. Chrome shipped an intervention in Chrome 55 that refuses to execute a parser-blocking, cross-origin script injected via `document.write` when the user is on a slow connection, logging a console warning instead. That is a rare case of the platform simply declining to honour an API call because the cost is unacceptable, and it is a good thing to be able to cite. ## What to use instead Every legitimate use has a modern equivalent: - Adding markup: `element.insertAdjacentHTML('beforeend', html)` or explicit DOM construction with `createElement` and `append`. - Loading a script conditionally: create a script element, set `src`, append it — non-blocking, with `load` and `error` events you can act on. - Rendering a whole document from script: build it and swap it into a container, or navigate. All of these are additive and none of them can silently reopen the document. ## Where you still meet it Legacy ad tags, older tag managers, and some payment or analytics snippets still ship `document.write`. When you inherit one, the practical moves are: wrap or replace it with a dynamic script insertion, or, if the vendor's snippet cannot be changed, isolate it in an iframe so a destructive write clears only the frame. The framing to give an interviewer is that this is not a style preference — the API's behaviour genuinely changes depending on whether the parser is still running, and that state-dependence is what makes it unusable in application code.
- Why does document.write from a deferred or async script cause trouble?Neither runs while the parser has an insertion point at their position. Depending on whether parsing has already finished, the write either destroys the document by implicitly reopening it or is dropped with no effect. The behaviour is timing-dependent, which is exactly what makes it unusable.
- How would you replace a third-party snippet that uses document.write to inject its script?Create the script element yourself, set `src` and `async` as appropriate, and append it — you get a non-blocking fetch plus `load` and `error` events. If the vendor snippet cannot be modified, sandbox it inside an iframe so a destructive write only clears that frame.
- What is the connection between document.write and classic scripts blocking the parser?It is the cause. Because a classic script can inject markup at its own position in the input stream, the parser cannot run ahead of it and must wait for the script to finish. That single capability is why synchronous execution is the default and why `async` and `defer` had to be opt-in.
saying these in an interview costs you the question
- Thinks document.write appends to the body
- Says it is only discouraged for style reasons
- Believes it works the same before and after load
- Assumes injected scripts load asynchronously
- Confuses it with innerHTML assignment