What actually happens when a page opens a synchronous XMLHttpRequest with xhr.open('GET', url, false), and why do browsers treat that as deprecated?
answer
- the third argument is async
- send() does not return
- one thread runs everything
- cannot even be given a timeout
- fine on a worker thread
basics
~20 sPassing false as the third argument to XMLHttpRequest's open() makes send() block the thread until the whole response arrives. On the main thread that freezes rendering, input and every other task for the duration, which is why browsers log a deprecation warning and restrict the mode.
solid answer
~50 sThe third argument to `open()` is `async`, and passing `false` makes `send()` block: it does not return until the response has been fully received or the connection fails. On a document's main thread that means the browser cannot render a frame, run a timer, dispatch a click, or process any other task for the whole round trip — a slow or hung server freezes the tab. Browsers log a console deprecation warning, and the specification actively narrows the mode: in a `Window` context, calling `open()` with `async` false while `xhr.timeout` is non-zero or `responseType` is anything but `''` throws `InvalidAccessError`, so a synchronous request cannot even be bounded by a timeout or ask for a typed response. Chromium also blocks synchronous XHR during page-dismissal handlers. The legitimate remaining home for it is inside a worker, where blocking that thread harms nobody; on the main thread the answer is always an async request.
code
javascript · 13 linesconst xhr = new XMLHttpRequest();
xhr.responseType = 'json';
try {
xhr.open('GET', '/data', false); // sync + typed response
} catch (err) {
console.log(err.name); // "InvalidAccessError" in a document context
}
// Text-only sync request: legal, but blocks everything until it completes
const plain = new XMLHttpRequest();
plain.open('GET', '/data', false);
plain.send();
console.log(plain.responseText);go deeper
Know that the third argument of open() controls async versus sync and that passing false freezes the page until the response arrives. Being able to say you would never use it on the main thread is the expected answer.
Explain the mechanics: send() does not return, so the single main thread cannot render, dispatch input or run timers for the whole round trip, and describe the InvalidAccessError that blocks pairing it with timeout or responseType.
Diagnose it in the wild — a long task bottoming out in send(), the console deprecation warning, inflated input-delay metrics — and describe the structural replacement rather than a one-line swap, including inlining bootstrap config into the HTML.
Own the policy angle: decide how blocking calls are kept out of the codebase over time via lint rules, vendor-script review and long-task budgets, and where a worker thread is the sanctioned place for straight-line blocking work.
## What the third argument does `xhr.open(method, url, async, user, password)` — `async` defaults to `true`. Passing `false` puts the object in synchronous mode, and the effect is entirely on `send()`: ```js const xhr = new XMLHttpRequest(); xhr.open('GET', '/config.json', false); xhr.send(); // returns only when the response is complete console.log(xhr.responseText); // already populated ``` The appeal is obvious — no callbacks, no state machine, the value is simply there on the next line. The cost is what makes it a deprecated feature. ## Why it is a main-thread hazard A document has one main thread, and it runs everything: script, the browser's event loop turn, style and layout, and the frame the compositor is waiting for. A blocking `send()` occupies that thread for the entire network round trip. During that window nothing else can be dispatched — a timer that came due, a click the user made, a scroll, a `MutationObserver` callback, an animation frame. The tab does not merely feel slow; it is unresponsive, and the operating system may show the "page is not responding" affordance. The duration is not yours to control. A same-origin request on a fast connection may take 20 ms in development and 8 seconds on a congested mobile network, and you cannot cap it — see the timeout restriction below. A hung server holds the tab until the browser's own network timeout fires. There is also a correctness dimension. Because no other task can interleave, everything the page would have done during that time is deferred and then executes in a burst, which produces layout jank and, in code that assumed interleaving, ordering surprises. ## The restrictions the platform imposes The specification does not merely discourage the mode — it removes capabilities from it inside a `Window`: - `open()` throws `InvalidAccessError` if `async` is `false` and either `xhr.timeout` is non-zero or `responseType` is not the empty string. You may not bound the block with a timeout, and you may not ask for a typed response; text is all you get. - Progress notifications are effectively absent — a synchronous request has no meaningful intermediate states to observe, since no script can run while it is in flight. - Chromium blocks synchronous XHR issued from page-dismissal handlers, which was historically the excuse people gave for using it. That first rule is the concrete gotcha in real code: the following throws, and the message names an error people do not recognise. ```js const xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.open('GET', '/data', false); // InvalidAccessError in a document ``` ## Where it is still legitimate Inside a `Worker` — a dedicated worker, a shared worker, or a service worker's startup script context — `XMLHttpRequest` is available and synchronous mode is not restricted in the same way, because blocking a worker thread does not block rendering or input. Worker code that wants a straight-line fetch of some configuration, without restructuring around callbacks, can legitimately use it. Even there it is a design smell if the worker also serves messages, because a blocked worker cannot process its message queue either. The other place it survives is old code you inherit: bootstrap scripts loading configuration before anything else, or legacy libraries. Those are the ones that show up as multi-second freezes in a performance profile as a single long task attributed to `send()`. ## How to replace it The replacement is structural, not a one-line swap: make the call asynchronous and give the code somewhere to wait. Wrap the XHR in a promise and `await` it, or use `fetch`. When the synchronous call sat at page bootstrap to fetch configuration before rendering, the better fixes are to inline the configuration into the initial HTML, or to render a shell and fill it in when the data lands. ## How to spot it in a codebase Grep for the literal third argument — `open(` followed by `, false)` — and check any library that has not been touched in years. In the browser, the console prints a deprecation warning naming synchronous XHR on the main thread, and a performance trace shows one long task whose stack bottoms out in `send()`. Because the block is synchronous, it will also inflate the metrics that track long tasks and input delay.
- Why does setting xhr.timeout before a synchronous open() throw instead of just being ignored?Because the specification forbids the combination in a `Window` context outright: `open()` throws `InvalidAccessError` when `async` is `false` and `timeout` is non-zero or `responseType` is not the empty string. Silently ignoring it would let developers believe a blocking call was bounded when it was not, so the platform makes the impossibility loud instead of leaving a false sense of safety.
- Is synchronous XHR acceptable inside a Web Worker?Yes, with judgment. A worker has its own thread, so blocking it does not stall rendering or input, and the `Window`-only restrictions on timeout and responseType do not apply there. The remaining caveat is that a blocked worker cannot process its own message queue, so a worker that also serves requests from the page should still prefer async calls.
- How would you find synchronous XHR in an inherited codebase?Grep for `open(` with a third argument of `false`, including inside bundled vendor code. At runtime the console prints a deprecation warning for main-thread synchronous XHR, and a performance profile shows a single long task whose stack bottoms out in `send()`. Long-task and input-delay metrics in field data will also spike on the pages that run it.
A synchronous XHR is a cashier who stops mid-transaction to phone the warehouse and refuses to do anything until someone answers — the queue does not move, no matter how many customers are waiting.
saying these in an interview costs you the question
- Thinks synchronous XHR only blocks the current function
- Believes xhr.timeout can bound a synchronous request
- Says it is fine because the request is usually fast
- Claims it was removed from browsers entirely
- Confuses blocking the main thread with blocking the network