A script element is created with document.createElement('script'), given a src, and appended to the head at runtime. Does it block the HTML parser, and in what order do several scripts created this way execute?
answer
- not parser-inserted, so never blocking
- the default is not what markup would give
- order follows the network, not your loop
- one boolean set before insertion fixes it
- load and error events, not a return value
basics
~20 sA dynamically inserted script never blocks the HTML parser and behaves as async by default: each one runs as soon as its own download finishes, so order is unpredictable. Setting script.async = false before appending restores insertion-order execution.
solid answer
~50 sA script element created in JavaScript is not parser-inserted, so it never blocks HTML parsing, and its `async` property defaults to `true` regardless of the markup you would have written. Each such script therefore executes the moment its own fetch completes, which means several of them run in download-completion order — effectively unpredictable, and a classic source of "library is undefined" bugs in hand-rolled loaders. The fix is a genuine platform feature, not a hack: set `script.async = false` **before** inserting the element. That clears the forced-async behaviour and puts the script into the ordered list, so scripts marked this way execute in the order they were inserted while still never blocking the parser. For load and failure handling you use the element's `load` and `error` events; there is no synchronous way to wait for it.
code
javascript · 17 linesfunction loadOrdered(urls) {
return Promise.all(
urls.map(
(src) =>
new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = src;
s.async = false; // must be set before insertion
s.addEventListener('load', () => resolve(src));
s.addEventListener('error', () => reject(new Error(src)));
document.head.append(s);
})
)
);
}
loadOrdered(['/lib.js', '/plugin.js']);go deeper
Know that assigning src does nothing until the element is added to the document, and that you find out it finished by listening for its load event rather than by any return value.
Explain that script-created elements default to async, so execution follows download completion rather than insertion order, and name async = false before insertion as the ordered alternative.
Be ready to explain why the resulting failure is intermittent and cache-dependent, and to distinguish a fetch failure from a script that arrived and threw when designing retry behaviour.
Own when dynamic loading is justified at all — consent gates, flags, third-party widgets — against the round-trip cost, and set the rules for how third-party tags are inserted and monitored across an application.
## Parser-inserted versus script-inserted The platform distinguishes scripts the HTML parser created from scripts your code created. Only the former can block parsing, because only the former sit at a defined position in the token stream. A script element you build yourself is outside that stream entirely: ```js const s = document.createElement('script'); s.src = '/analytics.js'; document.head.append(s); ``` This never stalls parsing, even if the document is still being parsed when it runs. The fetch is kicked off when the element is inserted into the document — setting `src` alone does nothing until insertion. ## The forced-async default For a script element that was not parser-inserted, the `async` IDL attribute is `true` by default. The consequence is that the script executes as soon as its own download completes. Insert three of them in a loop and their execution order is whichever finished first — usually the smallest file, sometimes the warmest cache entry, and different on every network profile. ```js ['/lib.js', '/plugin.js', '/app.js'].forEach((src) => { const s = document.createElement('script'); s.src = src; document.head.append(s); }); // execution order: unspecified ``` This is exactly the bug shape that passes locally with everything in cache and fails one time in twenty in production, with a stack trace pointing at the dependent file rather than the loader. ## Restoring order with async = false The platform provides an explicit escape hatch. Setting `async` to `false` on a script-created element **before** it is inserted removes the forced-async behaviour and puts the element into the ordered execution list: ```js ['/lib.js', '/plugin.js', '/app.js'].forEach((src) => { const s = document.createElement('script'); s.src = src; s.async = false; // must be set before insertion document.head.append(s); }); // execution order: lib.js, plugin.js, app.js ``` All three still download in parallel, none blocks the parser, and evaluation happens in insertion order. This is the mechanism every hand-written script loader relies on, and it is the answer the question is really fishing for. Two details matter: the assignment must happen before the element enters the document, and the ordering guarantee holds among the scripts marked this way — a separate forced-async script inserted alongside them stays unordered. ## Handling completion and failure There is no synchronous signal. You listen on the element: ```js s.addEventListener('load', onReady); s.addEventListener('error', onFailed); ``` The `error` event fires for a network failure or a non-successful response, but **not** for an exception thrown inside the script — that surfaces as a normal uncaught error on `window`. Distinguishing "never arrived" from "arrived and threw" is a question people get wrong, and it changes whether a retry is sensible. A cross-origin script also reports exceptions opaquely as `"Script error."` unless the response carries the appropriate CORS header and the element carries `crossorigin`, which is worth setting on any third-party script whose failures you intend to monitor. ## When to reach for this at all Dynamic insertion is right when the decision to load is conditional: a consent gate, a route that needs a heavy editor, a feature flag, a third-party widget that should only appear for some users. It is the wrong tool for the application's own entry point, where a `defer` or module tag in the markup is discovered earlier and needs no code to run first. A dynamically inserted script cannot be found until the script that inserts it has itself downloaded and executed, so every such load starts at least one round trip late. If the URL is known up front but the decision is not, `<link rel="preload" as="script">` can warm the cache so the later insertion resolves quickly. ## The short answer Never parser-blocking; async by default; `async = false` before insertion buys you insertion-order execution; use the `load` and `error` events, and remember that `error` does not fire for a throwing script.
- Does setting script.async = false after appending the element still work?No. The forced-async behaviour is resolved when the element is inserted into the document, so the assignment has to happen first. Setting it afterwards leaves the script in the unordered group, which is why loader code always configures the element fully before appending it.
- Does the error event fire if the script downloads successfully but throws while executing?No. The element's `error` event covers fetch failures and non-successful responses only. An exception during evaluation propagates as an uncaught error on `window`, so a loader that only listens for `error` will treat a broken-but-delivered script as loaded successfully.
- When would you prefer a defer tag in the markup over inserting a script from JavaScript?Whenever the URL is known at render time and the load is unconditional. A markup tag is discovered as the document is parsed, so the fetch starts immediately, whereas a dynamically inserted script cannot begin until the inserting script has itself been fetched and run — at least one extra round trip.
saying these in an interview costs you the question
- Thinks a script inserted from JS blocks parsing
- Assumes insertion order equals execution order
- Sets async = false after appending the element
- Believes the error event covers runtime exceptions
- Uses dynamic insertion for the main app bundle