skip to content

Network APIs

You will learn the browser-side APIs for talking to a server: fetch and its streaming body, legacy XHR, SSE, WebSocket, and beacon sends. Interviewers ask because every data-fetching library you name is a thin wrapper over these, and the wrappers hide the failure modes.

on this pageshow

explore

questions

29

A fetch() call receives an HTTP 500 response from the server, yet the .catch() handler never runs and the .then() branch executes instead. Why does the Fetch API behave this way, and how do you detect the failure?

level: juniorimportance: must knowfreq 85%

answer

  1. a response is still a response
  2. rejection means no response arrived
  3. the ok flag, not the promise
  4. throw it yourself before parsing
  5. TypeError versus HTTP status

basics

~20 s

fetch() resolves for any completed HTTP exchange, including 404 and 500, and rejects only when no response arrives at all — a network or DNS failure, or a blocked request. Detect HTTP failures yourself with response.ok or response.status.

solid answer

~40 s

The Fetch Standard treats "the server answered" as success, whatever it answered. A 500 is a perfectly well-formed HTTP response, so the promise fulfils with a `Response` object whose `status` is 500 and whose `ok` is `false`. `fetch()` rejects only when there is no response to hand back: DNS failure, connection refused or reset, a TLS error, a request blocked by CORS or Content-Security-Policy, a malformed URL, or an abort. Those reject with a `TypeError` (an `AbortError` `DOMException` for aborts) carrying deliberately little detail, because leaking why a cross-origin request failed would expose information about another origin. So every real wrapper checks `if (!response.ok) throw …` before touching the body — otherwise a 500 whose body is an HTML error page silently reaches `response.json()`, which then rejects with a confusing parse error.

go deeper

for a junior

Be able to state plainly that fetch only rejects when no response came back, and show the if (!response.ok) throw … guard before parsing. Knowing ok means status 200–299 is expected.

for a middle

Explain which conditions actually reject — DNS, connection, TLS, CORS or CSP block, bad arguments, abort — and that they surface as a TypeError, with abort as an AbortError instead. Say why the error is deliberately vague.

for a senior

Show the wrapper you would ship: read the body before throwing, distinguish transport failure from an application-level status, and handle bodyless 204 responses. Explain why conflating the two failure kinds produces misleading incident reports.

for a principal

Own the contract across the codebase: whether non-2xx becomes a thrown error or a returned result type, how status and server payload travel to logging and telemetry, and how that choice interacts with retry policy for genuinely retryable transport failures.

## The rule in one line `fetch()` rejects only when the browser could not obtain a response at all. Anything the server actually said — 200, 301, 404, 500 — is a *successful* outcome for the promise. The HTTP status is data about the exchange, not an error in the exchange. ## Why the API was designed this way A `Response` object models a completed HTTP message: a status line, headers, and a body. When a server replies "500 Internal Server Error", the request was sent, the connection worked, and a full response came back. From the transport's point of view nothing failed, so the Fetch Standard fulfils the promise and hands you the `Response` to inspect. Rejection is reserved for the cases where there is genuinely nothing to inspect: - DNS resolution failed, or the connection was refused, reset, or timed out at the transport level - the TLS handshake failed - the browser refused to make or expose the request: a CORS check failed, Content-Security-Policy blocked it, or mixed content was blocked - the arguments were invalid: a malformed URL, an illegal method such as `CONNECT`, or a forbidden header - the request was aborted through an `AbortSignal` All of these reject with a `TypeError`, except abort, which rejects with an `AbortError` `DOMException`. The `TypeError` is intentionally vague — it does not tell you whether the host was unreachable or the CORS check failed, because that difference would let a page probe another origin's network state. ## Reading the outcome correctly The `Response` gives you everything you need: - `response.ok` — `true` exactly when `status` is in the 200–299 range - `response.status` and `response.statusText` - `response.headers`, a `Headers` object - `response.redirected` — whether the browser followed one or more redirects to get here - `response.type` — `"basic"`, `"cors"`, `"opaque"`, or `"opaqueredirect"` A correct wrapper converts a non-`ok` response into a thrown error itself, and does so *before* parsing: ```js async function getJson(url, init) { const response = await fetch(url, init); if (!response.ok) { const detail = await response.text(); throw new Error(`HTTP ${response.status} ${response.statusText}: ${detail.slice(0, 200)}`); } return response.json(); } ``` Note the order: read the body as text *first*, so the server's error message survives into the thrown error, then throw. Throwing before reading the body discards the most useful diagnostic you had. ## The second trap: the parse error that looks like a network error Skip the `ok` check and a 500 that returns an HTML error page flows straight into `response.json()`. That call rejects with a `SyntaxError` about an unexpected `<`. Your `catch` block fires, so it *feels* like fetch rejected on the 500 — and people conclude fetch does reject on 5xx. It did not; the parse did. The distinction matters the moment you try to log the status, because by then you have thrown away the `Response`. ## Empty bodies `ok` is also `true` for 204 No Content, which by definition has no body. `response.json()` on an empty body rejects with a parse error, so a wrapper that always calls `json()` breaks on the first endpoint that returns 204. Guard on the status, or read `text()` and only parse when the string is non-empty. ## Comparing with what people expect Most HTTP client libraries built on top of the platform reject on non-2xx by default, which is why the fetch behaviour surprises people arriving from one. Neither choice is wrong; fetch is simply lower-level, and the layer that decides "a 404 is an error for my application" is your code. Some APIs legitimately use 404 as a normal answer ("no such record yet") or 409 as an expected outcome, and a client that rejects on every non-2xx forces you to unwrap errors to find them. ## What to do in practice Write one helper, use it everywhere, and make it distinguish three outcomes: promise rejection (no response — retry or report offline), a non-`ok` response (an application-level failure with a status and a body worth surfacing), and success. Conflating the first two is how "the server is down" ends up in a user's face when the real answer was 403.

  • What exactly does fetch reject with, and how much can you learn from that error?
    Network-level failures reject with a `TypeError`; an aborted request rejects with an `AbortError` `DOMException`. The `TypeError` is deliberately uninformative — it does not distinguish a refused connection from a CORS block, because telling a page why a cross-origin request failed would leak the state of another origin. If you need to tell them apart, you need server-side or devtools evidence, not the error object.
  • Why should a wrapper read the response body before throwing on a non-ok status?
    Because the body usually carries the server's explanation — a JSON problem document, a validation list, a stack trace in dev. Once you throw, the `Response` is gone and the body can never be read. Reading `await response.text()` first, then throwing with the status and a truncated body attached, keeps the only diagnostic the server gave you.
  • A wrapper checks response.ok and then calls response.json() on every request. Where does that break?
    On any response with no body. A 204 No Content passes the `ok` check because 204 is in the 2xx range, but `json()` on an empty body rejects with a parse error. Guard it: return `null` for 204, or read `text()` and only `JSON.parse` when the string is non-empty.

saying these in an interview costs you the question

  • Says fetch rejects the promise on 4xx or 5xx statuses
  • Wraps fetch in try/catch and assumes anything after it succeeded
  • Treats a caught error as proof the network failed
  • Checks status === 200 only, missing 201 and 204
  • Throws on a bad status without first reading the body

context

open as a page

A server-sent events stream in the browser delivers messages tagged with an event name such as `priceUpdate`, but the page's `eventSource.onmessage` handler never fires. Why does that happen, and how do you receive those messages?

level: juniorimportance: must knowfreq 60%

basics

~10 s

EventSource dispatches a named message as an event of that name, so onmessage never sees it. Register eventSource.addEventListener('priceUpdate', handler) instead, and expect event.data as a string you parse yourself.

open as a page

In the browser, fetch() has no timeout option. How do you make a fetch request give up after five seconds?

level: juniorimportance: must knowfreq 62%

basics

~10 s

Drive it from an abort signal: fetch(url, { signal: AbortSignal.timeout(5000) }) aborts the request when the timer fires. The manual equivalent is an AbortController plus setTimeout calling controller.abort(), with the timer cleared afterwards.

open as a page

In the browser you write `const ws = new WebSocket(url); ws.send('hello');` on the very next line. What happens, and what does the WebSocket readyState tell you about when send() is legal?

level: juniorimportance: must knowfreq 68%

basics

~10 s

The send() call throws an InvalidStateError, because the socket is still in the CONNECTING state — the constructor only starts the connection. Send once the open event has fired and readyState is WebSocket.OPEN.

open as a page

A page calls fetch('/collect', { method: 'POST', body }) from inside a pagehide handler, and the request frequently never reaches the server. Why does the browser drop it, and what does navigator.sendBeacon do differently?

level: middleimportance: must knowfreq 65%

basics

~20 s

An ordinary fetch is tied to the document that started it, so when the document is destroyed the browser aborts the request still in flight. sendBeacon marks the request as keepalive, handing it to the browser to complete after the page is gone.

open as a page

Your page's EventSource keeps firing its `error` event and the Network panel shows the request being re-issued, yet nothing in your code reopened the stream. What is EventSource doing, and when does it stop retrying on its own?

level: middleimportance: must knowfreq 58%

basics

~20 s

EventSource reconnects by itself after a dropped connection, firing error each time. Check readyState: CONNECTING means it will retry, CLOSED means it gave up. It gives up on close(), or when the response is not 200 with Content-Type text/event-stream.

open as a page

A browser WebSocket fires an error event with no details and then a close event with code 1006, an empty reason and wasClean false. What does that combination tell you about how the connection ended, and why does the API give you nothing more?

level: middleimportance: must knowfreq 52%

basics

~20 s

Code 1006 means the connection ended without a close handshake, so the browser synthesised the code locally rather than receiving one. The error event is deliberately detail-free for security, so 1006 alone cannot distinguish a rejected connection from a dropped network link.

open as a page

Walk through the readyState values an XMLHttpRequest passes through, and explain why modern XHR code listens for the load and error events instead of onreadystatechange.

level: middleimportance: must knowfreq 62%

basics

~20 s

An XMLHttpRequest moves through readyState 0 UNSENT, 1 OPENED, 2 HEADERS_RECEIVED, 3 LOADING and 4 DONE, firing readystatechange on each step. Modern code prefers the load, error, timeout, abort and loadend events because each names one outcome instead of requiring a state check.

open as a page

Your upload widget must show a live progress bar while a file is being sent to the server. Why does that still mean XMLHttpRequest rather than fetch, and which object do you attach the progress listener to?

level: middleimportance: must knowfreq 58%

basics

~20 s

XMLHttpRequest exposes an upload object, and progress events fired on xhr.upload report bytes sent. As of 2026 fetch has no equivalent upload-progress event, which is why file-upload widgets that show a live bar are still written against XHR.

open as a page

Session-end analytics is missing for a large share of mobile visits, and the app sends its final payload from a window 'unload' handler. Explain why that handler is unreliable and where the send should happen instead.

level: seniorimportance: must knowfreq 55%

basics

~20 s

On mobile a tab is usually backgrounded and later discarded or killed, so unload frequently never fires at all. Send at visibilitychange when the state becomes hidden, with pagehide as a backstop, using sendBeacon or a keepalive fetch.

open as a page

The browser WebSocket API performs no automatic reconnection and gives scripts no way to send a protocol-level ping. What must an application-level reconnect-and-heartbeat layer get right for a browser WebSocket client?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Reconnect with capped exponential backoff plus jitter, skip reconnecting after a deliberate client close or a permanent application close code, and resubscribe and resync state on every new socket. Heartbeats must be ordinary application messages on a timer, since scripts cannot send WebSocket ping frames.

open as a page

What does the browser's navigator.sendBeacon(url, data) call return, and what does that return value actually tell you about whether the server received the data?

level: juniorimportance: should knowfreq 45%

basics

~20 s

navigator.sendBeacon returns a boolean immediately: true means the browser queued the data for sending, false means it refused to queue it, usually because the payload was too large. True is not a delivery receipt, and no response is ever readable.

open as a page

In XMLHttpRequest, what does setting `xhr.responseType = 'json'` change about how you read the result, and why does reading `xhr.responseText` afterwards throw?

level: juniorimportance: should knowfreq 38%

basics

~20 s

Setting XMLHttpRequest's responseType to 'json' makes the browser parse the body for you and expose the parsed value on xhr.response. Reading xhr.responseText then throws an InvalidStateError, because that getter is only legal when responseType is the empty string or 'text'.

open as a page

What does the keepalive: true option on the browser's fetch() give you that navigator.sendBeacon does not, and what limit does the browser place on keepalive request bodies?

level: middleimportance: should knowfreq 45%

basics

~20 s

fetch with keepalive gives the same survive-the-page guarantee as sendBeacon while letting you choose the method, set headers such as Content-Type, control credentials, and read the response. The cost is a shared in-flight body budget of about 64 KiB per document.

open as a page

A file upload sent with fetch() and a FormData body starts failing on the server as soon as a developer adds headers: { 'Content-Type': 'multipart/form-data' } to the init object. Why does setting that header break the upload?

level: middleimportance: should knowfreq 42%

basics

~20 s

A multipart body is split by a random boundary token the browser generates and normally advertises in the Content-Type header it sets for you. A hand-written multipart/form-data header has no boundary parameter, so the server cannot parse the parts. Omit the header.

open as a page

Why can the body of a Response returned by fetch() be read only once — so a second call to response.json() throws — and what do you do when two pieces of code both need that body?

level: middleimportance: should knowfreq 48%

basics

~20 s

A fetch Response body is a one-shot stream, not a stored string. The first read consumes it and sets response.bodyUsed to true, so a second read throws a TypeError. Call response.clone() before reading if two consumers need it.

open as a page

A page calls fetch() against its API and the session cookie is not attached to the request, even though the same cookie is visible in devtools. What does the credentials option in the fetch init control, and what is its default?

level: middleimportance: should knowfreq 55%

basics

~20 s

credentials controls whether the browser attaches cookies, HTTP authentication entries and TLS client certificates to a fetch request, and whether it stores Set-Cookie from the response. It defaults to 'same-origin', so cross-origin calls need credentials: 'include'.

open as a page

You need to open an EventSource in the browser against an API on another origin and have the request authenticated. What can and cannot the EventSource constructor do about request headers and credentials, and what has to be true on the server?

level: middleimportance: should knowfreq 45%

basics

~20 s

EventSource accepts only a URL and { withCredentials }. It cannot set an Authorization header, cannot change the method from GET, and sends no body. Cross-origin credentialed streams need the server to allow that exact origin and credentials.

open as a page

Using the browser Fetch API, how do you consume a response incrementally instead of awaiting response.text(), and what problem does piping response.body through a TextDecoderStream solve?

level: middleimportance: should knowfreq 45%

basics

~20 s

response.body is a ReadableStream of byte chunks: call getReader() and loop on read() until done. Piping it through TextDecoderStream turns those bytes into text correctly even when a multi-byte character is split across two chunks.

open as a page

In the browser, a WebSocket receives binary messages and your handler finds that event.data is a Blob. What does the socket's binaryType property control, which values does it accept, and when should you set it?

level: middleimportance: should knowfreq 38%

basics

~20 s

binaryType chooses how a browser WebSocket hands you binary messages: 'blob' (the default) or 'arraybuffer'. Set it to 'arraybuffer' immediately after constructing the socket when you need to read bytes synchronously instead of doing an asynchronous Blob read.

open as a page

A retry wrapper builds one Request object for a POST and passes that same object to fetch() on every attempt. The first attempt fails and the retry throws a TypeError before any network activity happens. What is wrong, and how do you write the wrapper correctly?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A Request carries a one-shot body, and sending it consumes that body. The second fetch on the same Request finds bodyUsed already true and throws a TypeError. Clone the Request before each attempt, or rebuild it from the URL and init.

open as a page

In a single-page app that opens an EventSource when a dashboard route mounts, users report that after moving between routes for a while the page stops loading anything and new requests sit pending forever. How would you diagnose this, and what is the fix?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Each route visit opens a stream that nothing closes, and an EventSource stays open and self-reconnects until close() is called. Over HTTP/1.1 the leaked streams occupy the browser's roughly six connections per origin, so every other request queues.

open as a page

You call fetch() in the browser, look at the response headers, and then decide you do not need the payload — so you never read response.body and just drop the reference. What actually happens to the transfer, and what should you have done?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Dropping the reference does not end the transfer: the body stream sits un-consumed and its resources are held until it is cancelled or the object is collected. Call response.body.cancel() to discard the body, or controller.abort() to kill the whole request.

open as a page

A search box fires a fetch on every keystroke and renders whatever comes back. Users occasionally see results for a query they have already replaced. What is happening, and how do you fix it?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Responses complete out of order, so a slower earlier request can land after a newer one and overwrite it. Cancel the previous request with its own AbortController before starting the next, and additionally drop any response whose query no longer matches the current input.

open as a page

A browser page calls socket.send() many times a second on a WebSocket. On a slow network the tab's memory climbs and the UI stalls, yet send() never throws. What does socket.bufferedAmount tell you, and how do you use it to apply backpressure?

level: seniorimportance: should knowfreq 40%

basics

~20 s

socket.bufferedAmount is the number of bytes passed to send() that the browser has not yet put on the network. send() never blocks or fails on a slow link, so an unbounded producer simply grows that queue; sampling bufferedAmount and pausing above a threshold is the only backpressure the API offers.

open as a page

What actually happens when a page opens a synchronous XMLHttpRequest with xhr.open('GET', url, false), and why do browsers treat that as deprecated?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Passing 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.

open as a page

In a page built on XMLHttpRequest, some requests never settle and the spinner stays up forever. How do you bound and cancel an XHR, and which events fire in each case?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Set xhr.timeout in milliseconds to bound a request and call xhr.abort() to cancel one. A timeout fires the timeout event, an abort fires the abort event, and neither fires load — which is why teardown belongs in loadend, the event that fires on every outcome.

open as a page

You are designing a one-way live feed of order-status updates for a web app, and the team's instinct is to reach for WebSockets. How would you decide between server-sent events, WebSockets, and periodic polling?

level: principalimportance: should knowfreq 42%

basics

~20 s

Match the transport to the traffic shape. A one-way feed suits server-sent events, which ride ordinary HTTP and reconnect themselves; WebSockets earn their extra machinery only with real client-to-server chat; polling wins when updates are rare and latency tolerance is loose.

open as a page

Your telemetry endpoint expects JSON, but requests sent with navigator.sendBeacon('/collect', JSON.stringify(payload)) arrive with Content-Type: text/plain;charset=UTF-8. Why does that happen, and what are your options?

level: middleimportance: nice to knowfreq 35%

basics

~20 s

sendBeacon accepts no headers, so the Content-Type is derived from the body type, and a string always becomes text/plain;charset=UTF-8. Either parse text/plain on the server, send a Blob typed application/json, or use fetch with keepalive and explicit headers.

open as a page