Web Platform & APIs
Browser platform APIs — fetch and XHR, client storage and IndexedDB, web and service workers, postMessage, server-sent events, and PWAs.
12 questions
JuniorTheoryVery commonWhat does fetch() return, and how do you read JSON from the response?
What does fetch() return, and how do you read JSON from the response?
fetch(url) returns a Promise that resolves to a Response object once headers arrive, not the body. The body is read with another promise-returning method like response.json() or response.text(). So you typically chain fetch(url).then(r => r.json()).then(data => ...), or use await. response.ok tells you if the status was 2xx.
Common mistakes
- ✗Thinking the
fetchpromise resolves with the body — it resolves with aResponse, body needs another read - ✗Treating
response.json()as synchronous — it returns a promise that must be awaited - ✗Forgetting that
fetchresolves once headers arrive, before the body is downloaded
Follow-up questions
- →Why does
response.json()itself return a promise rather than the parsed value? - →What happens if you call
response.json()twice on the same response?
JuniorTheoryCommonHow do localStorage, sessionStorage, and IndexedDB differ as client storage?
How do localStorage, sessionStorage, and IndexedDB differ as client storage?
localStorage and sessionStorage are synchronous string-only key/value stores (~5MB), scoped per origin. localStorage persists until cleared; sessionStorage lives only for the tab's session. IndexedDB is an asynchronous, transactional database for large structured data, including objects and blobs. A storage event fires on other same-origin tabs when localStorage changes, letting them sync.
Common mistakes
- ✗Thinking web storage can hold objects directly — values are coerced to strings
- ✗Believing the
storageevent fires in the tab that made the change - ✗Confusing
sessionStorage(per tab) withlocalStorage(persistent, per origin)
Follow-up questions
- →Why does
localStorage.setItem('k', {a:1})store the literal[object Object]? - →When would IndexedDB be the right choice over web storage?
JuniorTheoryOccasionalWhat is XMLHttpRequest, and what does its readyState track?
What is XMLHttpRequest, and what does its readyState track?
XMLHttpRequest (XHR) is the classic API for making HTTP requests from JavaScript, the basis of AJAX. You open(method, url, async) then send(). By default async is true, so the call is non-blocking and you handle results in the onreadystatechange callback. readyState steps 0→4 through the request lifecycle; 4 means done, and you then check status.
Common mistakes
- ✗Believing XHR is synchronous by default —
asyncistrueunless you opt out - ✗Confusing
readyState(lifecycle stage) withstatus(the HTTP code) - ✗Thinking XHR can only carry XML, when it handles JSON, text, and binary too
Follow-up questions
- →Why is synchronous XHR (
asyncset tofalse) discouraged on the main thread? - →How does
fetchimprove on XHR for handling request lifecycles?
JuniorTheoryOccasionalWhat is window.postMessage for, and why must you check the message origin?
What is window.postMessage for, and why must you check the message origin?
window.postMessage lets scripts in different windows or iframes talk across origins, bypassing the same-origin policy safely. The sender calls target.postMessage(data, targetOrigin); the receiver listens for a message event. You must verify event.origin (and ideally event.source) in the handler, because any page can post to you — trusting unchecked messages opens an XSS hole. Avoid the * wildcard target.
Common mistakes
- ✗Skipping the
event.origincheck, trusting whatever message arrives - ✗Using the
*wildcard as the target origin, leaking data to any origin - ✗Thinking the browser, not your handler, filters out untrusted senders
Follow-up questions
- →Why is passing
*as the target origin a security risk for the sender? - →How does checking
event.sourceadd protection beyondevent.origin?
JuniorTheoryOccasionalWhat is a Web Worker, and why can't it touch the DOM?
What is a Web Worker, and why can't it touch the DOM?
A Web Worker runs a script on a separate background thread, so heavy work doesn't block the main UI thread. It has no access to the DOM, window, or the parent document, because those are not thread-safe and only the main thread owns them. The worker and page communicate by passing messages with postMessage, never by sharing the page's objects directly.
Common mistakes
- ✗Trying to read or update the DOM from inside a worker — it isn't available there
- ✗Assuming a worker shares the page's variables instead of communicating by messages
- ✗Believing a worker runs on the main thread, so it can't actually parallelize
Follow-up questions
- →What globals (like
selforfetch) does a worker still have access to? - →How does the worker hand a DOM update back to the main thread to perform?
MiddleTheoryOccasionalHow does fetch improve on XHR, and why doesn't it reject on a 404?
How does fetch improve on XHR, and why doesn't it reject on a 404?
fetch is promise-based, so it composes with async/await instead of XHR's callbacks, and exposes a streaming response.body. Crucially, its promise only rejects on a network failure — an HTTP 404 or 500 still resolves, so you must check response.ok or response.status yourself. Cancellation isn't built in; you pass an AbortController's signal and call controller.abort().
Common mistakes
- ✗Expecting
fetchto reject on a 404 or 500 — only network errors reject - ✗Forgetting to check
response.okbefore reading the body - ✗Thinking a fetch can't be cancelled —
AbortControllerprovides the signal
Follow-up questions
- →How do you reuse one
AbortControllersignal to cancel several fetches at once? - →How would you wrap
fetchso non-okresponses reject like XHR'sonerror?
MiddleTheoryOccasionalWhat is a service worker's lifecycle, and how does it enable offline?
What is a service worker's lifecycle, and how does it enable offline?
A service worker is a background script that acts as a network proxy between the page and the network. It progresses through install (precache assets), activate (clean old caches), then fetch, where it intercepts requests and can answer from the Cache API for offline use. It has no DOM access and is event-driven, getting terminated when idle and restarted on demand, so state must live in IndexedDB or a cache, not globals.
Common mistakes
- ✗Storing state in service worker globals — it is killed when idle and restarted
- ✗Thinking a service worker can read or update the DOM directly
- ✗Believing offline works without intercepting
fetchand serving from a cache
Follow-up questions
- →Why must a service worker persist state in IndexedDB rather than globals?
- →What is the difference between the
installandactivateevents?
MiddleTheoryOccasionalHow do a page and a Web Worker exchange data, and what are Transferable objects?
How do a page and a Web Worker exchange data, and what are Transferable objects?
They communicate by message passing: each side calls postMessage(data) and listens via onmessage. The data is copied by the structured clone algorithm, so the worker gets an independent copy — no shared references. For large buffers, copying is costly, so you pass Transferable objects (like an ArrayBuffer) as the second argument; ownership moves to the receiver with zero copy, and the sender's reference becomes unusable.
Common mistakes
- ✗Assuming
postMessageshares a reference rather than a structured-clone copy - ✗Thinking the sender keeps a usable buffer after a Transferable is transferred
- ✗Believing messages are limited to JSON-serializable plain objects
Follow-up questions
- →Why is transferring an
ArrayBufferfaster than letting structured clone copy it? - →What happens if you read the sender's buffer after it has been transferred?
MiddleTheoryRareWhat kind of store is IndexedDB, and when is it preferable to web storage?
What kind of store is IndexedDB, and when is it preferable to web storage?
IndexedDB is an asynchronous, transactional, object-oriented client database for large amounts of structured data. All reads and writes go through transactions and complete via events or promises, never blocking the thread. It stores structured-clone values — objects, arrays, blobs — not just strings, and supports indexes for fast queries. Prefer it over web storage when you need big datasets, async access (e.g. from a worker), or queryable records.
Common mistakes
- ✗Treating IndexedDB as synchronous — every operation is async via events or promises
- ✗Thinking it stores only strings, when it holds structured-clone objects and blobs
- ✗Forgetting that all access happens inside transactions
Follow-up questions
- →Why can IndexedDB be used inside a worker but
localStoragecannot? - →What types can the structured clone algorithm serialize that
JSON.stringifycannot?
MiddleTheoryRareWhat are server-sent events via EventSource, and how do they differ from WebSocket?
What are server-sent events via EventSource, and how do they differ from WebSocket?
Server-sent events let a server push updates to the browser over one long-lived HTTP connection. The client opens new EventSource(url) and handles onmessage/onopen/onerror; the channel is one-way, server→client, text only. The browser auto-reconnects on drop and can resume via Last-Event-ID. WebSocket, by contrast, is a separate full-duplex protocol carrying binary and text in both directions.
Common mistakes
- ✗Thinking SSE is bidirectional — it is one-way, server to client only
- ✗Building manual reconnection logic when
EventSourcereconnects automatically - ✗Expecting SSE to carry binary frames, when it is text-only
Follow-up questions
- →How does the
Last-Event-IDheader let a reconnecting client resume the stream? - →When would you pick WebSocket over SSE for a feature?
SeniorTheoryRareWhat pieces make a Progressive Web App offline-capable and installable?
What pieces make a Progressive Web App offline-capable and installable?
A PWA is a website that behaves like an installed app. The core pieces: a service worker that intercepts fetch and serves cached assets for offline use; the Cache API plus IndexedDB for the app shell and data; and a web app manifest (name, icons, display, start_url) that makes it installable to the home screen. An offline-first strategy caches the shell on install, then serves it instantly while updating in the background.
Common mistakes
- ✗Thinking the manifest, not the service worker, is what provides offline support
- ✗Believing offline works automatically once a site is on HTTPS
- ✗Forgetting the app shell must be precached on
installfor instant offline loads
Follow-up questions
- →How does a cache-first app shell strategy keep updating content in the background?
- →Which manifest fields are required for a browser to offer the install prompt?
SeniorTheoryRareCompare polling, long-polling, SSE, and WebSocket for realtime updates.
Compare polling, long-polling, SSE, and WebSocket for realtime updates.
Short polling repeatedly fetches on a timer — simple but wasteful and laggy. Long-polling holds each request open until data is ready, then reconnects — fewer empty round-trips but still one connection per message. SSE keeps one HTTP stream open for server→client text push with auto-reconnect — ideal for feeds. WebSocket upgrades to a persistent full-duplex channel for low-latency two-way binary/text — best for chat and games, at higher complexity.
Common mistakes
- ✗Confusing long-polling with a persistent socket — it still reconnects per message
- ✗Picking WebSocket for one-way feeds where SSE is simpler and auto-reconnects
- ✗Assuming all four have equal latency and bandwidth cost
Follow-up questions
- →Why does long-polling reduce empty responses but not the connection-per-message cost?
- →What makes WebSocket's handshake start as an HTTP upgrade request?