Detailed explanation
fetch and XMLHttpRequest (XHR) both let JavaScript make HTTP requests without reloading a page, but they expose different programming models. fetch returns a Promise and works naturally with async/await, making sequential requests and error handling easier to read. XHR uses an event-driven interface: you create an instance, call open(), attach handlers such as onload and onerror, then call send(). XHR is usually more verbose, although you can wrap it in a Promise.
The most important difference is error handling. fetch resolves with a Response even when the server returns HTTP errors such as 404 or 500. Check response.ok, which is true for status codes 200–299, or inspect response.status before processing the result. For example: const res = await fetch('/api/x'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); Fetch can reject for network failures, CORS failures, cancellation, and certain invalid requests—not just network failure. XHR also requires explicit status checking; an HTTP error normally triggers load rather than error.
Fetch separates receiving the response from consuming its body. Its Promise normally resolves once response headers are available, while response.json() or response.text() asynchronously reads the body. In browsers, response.body exposes a ReadableStream, enabling incremental processing instead of buffering the entire payload. A body is generally consumable only once; use response.clone() before reading if you need another copy. XHR can report download progress and expose partial text in some modes, but does not provide the same standard readable-stream interface.
Both APIs support cancellation: fetch uses an AbortController signal, while XHR provides abort(). XHR also has a timeout property and built-in upload progress events through xhr.upload. Fetch has no equivalent built-in upload progress event API; timeouts require an abort signal, such as AbortSignal.timeout() where supported. Both obey browser security rules, including CORS. Interviewers look for these behavioural distinctions and practical trade-offs, not simply the claim that fetch is newer and therefore always better.
Key points to remember
- fetch is Promise-based and integrates with async/await; XHR is event-driven.
- Neither API treats HTTP 404 or 500 as a network failure; check the HTTP status explicitly.
- Fetch exposes response bodies as streams, while json() and text() consume the body asynchronously.
- Cancel fetch with AbortController and XHR with abort(); XHR also provides a timeout property.
- XHR provides built-in upload progress events, making it useful for upload progress bars.
Common mistakes candidates make
- Assuming a catch block around fetch automatically handles HTTP error statuses.
- Forgetting to await response.json(), or trying to consume the same response body twice.
- Claiming fetch always replaces XHR without considering upload progress and timeout requirements.
Likely follow-up questions
Why are there two await operations in a typical fetch request?
The first waits for the Response, normally available when headers arrive. The second waits for the body to be read and parsed; response.json() can reject if the body is not valid JSON.
How do you cancel a fetch request?
Create an AbortController and pass its signal in the fetch options. Calling controller.abort() cancels the request or ongoing body consumption, typically causing an AbortError rejection.
How do cookies work with fetch compared with XHR?
Fetch sends cookies on same-origin requests by default; use credentials: 'include' for cross-origin requests that need cookies. For cross-origin XHR, set withCredentials to true; either API still requires appropriate server CORS headers and remains subject to cookie restrictions.
Interview tip: Start with the Promise-versus-events distinction, demonstrate the response.ok check, and finish with one practical reason to choose XHR.
