Web Platform & Browser APIs Interview Questions
Reviewed by Mark Dickie · Last updated
Web Platform APIs are interfaces built into the browser that let JavaScript access network resources, persistent storage, background threads, and device sensors without external libraries. For an interview on browser APIs, you should know how Fetch differs from XMLHttpRequest, how the Event Loop interacts with Web Workers, when to use IndexedDB versus localStorage, and how Service Workers enable offline caching. You should also understand the Promise-based patterns most modern APIs share, and be able to explain the same-origin policy and CORS headers from memory. These topics come up because interviewers want to see whether you can pick the right built-in tool instead of reaching for a library by reflex.
| API | Purpose | Key constraint |
|---|---|---|
| Fetch | HTTP requests | Promise-based, no built-in timeout |
| XMLHttpRequest | HTTP requests | Callback-based, legacy |
| localStorage | Key-value storage | ~5 MB limit, synchronous, blocks main thread |
| sessionStorage | Key-value per tab | Cleared when tab closes |
| IndexedDB | Structured object store | Asynchronous, large capacity, transactional |
| Web Workers | Background CPU threads | No DOM access, message-passing only |
| Service Workers | Offline proxy / cache | Separate thread, requires HTTPS |
| Intersection Observer | Scroll-based visibility | Asynchronous, replaces scroll-event listeners |
| WebSockets | Bi-directional streaming | Persistent connection, no per-message HTTP overhead |
What does a browser APIs interview test?
Interviews on the Web Platform probe whether you understand the request/response lifecycle in the browser and whether you can pick the right API for a given constraint: latency, storage size, offline support, or background processing. Expect to compare APIs that solve overlapping problems and to explain what happens on the main thread when each one runs.
How should you prepare for browser API interview questions?
- Read the MDN docs for each API you list on your resume. Interviewers often ask you to compare two APIs that solve the same problem.
- Write a small app that uses Fetch with async/await, handles errors, and aborts with AbortController. The fetch-with-timeout pattern comes up often.
- Build a Service Worker that caches assets and serves them offline. Be ready to explain the cache-versus-network strategy you chose and why.
- Store and retrieve structured data with IndexedDB. Know the transaction model and why it matters for concurrent reads.
- Offload a CPU-bound task to a Web Worker and measure the main-thread savings. Interviewers like concrete numbers.
What are the most common mistakes candidates make with browser APIs?
Confusing localStorage with IndexedDB is the most frequent error. localStorage is synchronous and blocks the main thread, so it is wrong for large or structured data. Another common gap: not knowing that Fetch does not reject on HTTP error status codes (404, 500), only on network failures. Candidates also forget that Service Workers require HTTPS except on localhost, and that they run in a separate context with no access to the DOM or window object.
Key facts
- Tarmac has 42 Web Platform interview questions on this topic, 25 of them on this page, at difficulty 2–5 of 5.
- Tarmac tracked 507 job postings asking for Web Platform in August 2026.
- Roles asking for Web Platform advertise a median base salary of US$167,700, across 145 job postings as of August 2026.
- Tarmac last reviewed these Web Platform interview questions on 31 August 2026.
At a glance
| Questions | 25 shown · 42 in the bank |
|---|---|
| Difficulty | 2–5 of 5 |
| Formats | Multiple choice, True / false, Find the bug, Fill in the blank, Multiple answer, Code output, Flashcard, Ordering, Short answer |
What you'll review
- storage apis
- popover api
- fetch api
- realtime transports
- web workers
- service workers
- view transitions
- observers
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
Web Platform/browser-apis/storage-apis
After localStorage.setItem("n", 42), what does typeof localStorage.getItem("n") evaluate to?#
Options
Show answer
typeof localStorage.getItem("n") evaluates to string. Web Storage stores only strings, so the number 42 is coerced to '42' on write and getItem always returns a string (or null for a missing key). To round-trip structured data, serialize with JSON.stringify on write and JSON.parse on read.
Web Storage stores only strings. The number 42 is coerced to the string "42" on write, and getItem returns a string (or null for a missing key). Serialize structured data with JSON.stringify on write and JSON.parse on read.
Web Platform/browser-apis/storage-apis
Data written to sessionStorage in one tab is automatically visible in another tab open to the same origin.#
Options
Show answer
False. sessionStorage is scoped to a single tab or browsing context and is cleared when that tab closes, so another tab cannot see it. The storage that is shared across all tabs of the same origin is localStorage, which also persists until explicitly cleared.
sessionStorage is scoped to a single tab/browsing context and is cleared when that tab closes, so other tabs cannot see it. localStorage is the one shared across all tabs of the same origin and persists until explicitly cleared.
Web Platform/browser-apis/popover-api
Clicking the button should open the menu using the Popover API, but nothing happens (and no error is thrown). Why?#
<button popovertarget="menu">Open menu</button>
<div id="menu" class="menu" hidden>
<a href="/profile">Profile</a>
</div>Options
Show answer
The <div> is missing the popover attribute, so it isn't a popover and popovertarget has nothing to control
popovertarget only does something when its target is an actual popover — i.e. the element carries the popover attribute. Here the <div> has hidden but not popover, so the button controls nothing and the click is a silent no-op. Fix: <div id="menu" popover> (and drop the manual hidden/display:none — a popover is hidden by default and the browser manages its visibility in the top layer). popovertarget correctly references the id, a <button> is a valid invoker, and popover invoker buttons don't submit forms.
Web Platform/browser-apis/storage-apis
To persist a value across browser sessions you call localStorage._____("key", "value"), and to read it back you call localStorage._____("key"), which returns a string or null.#
Show answer
To persist a value across browser sessions you call localStorage.**setItem**("key", "value"), and to read it back you call localStorage.**getItem**("key"), which returns a string or null.
setItem(key, value) writes a string entry and getItem(key) reads it (returning null for a missing key). Both store strings only — serialize objects with JSON.stringify first. removeItem(key) deletes one entry and clear() empties the store.
Web Platform/browser-apis/popover-api
To build a script-free disclosure, add the _____ attribute to the element you want to show/hide, and on the trigger button set _____ to that element's id. From JavaScript you can instead call el._____() to open it programmatically.#
Show answer
To build a script-free disclosure, add the **popover** attribute to the element you want to show/hide, and on the trigger button set **popovertarget** to that element's id. From JavaScript you can instead call el.**showPopover**() to open it programmatically.
The popover attribute marks the element as a popover (hidden until invoked, shown in the top layer). A button with popovertarget="<id>" toggles it with no JavaScript. The imperative equivalents are showPopover(), hidePopover(), and togglePopover(), plus the :popover-open pseudo-class and beforetoggle/toggle events.
Web Platform/browser-apis/fetch-api
A fetch() request gets back an HTTP 404 response. What happens to the returned promise?#
Options
Show answer
The promise resolves with a Response whose ok is false and status is 404. fetch only rejects on network-level failures like DNS errors, being offline, an abort, or a CORS block — a completed HTTP response is a success as far as the promise is concerned. You must inspect response.ok yourself and throw to treat error statuses as failures.
fetch only rejects on network failures (DNS error, offline, aborted, CORS block). Any completed HTTP response — including 404 or 500 — resolves the promise. You must check response.ok (or response.status) yourself and throw to treat error statuses as failures.
Web Platform/browser-apis/realtime-transports
In a dashboard's network tab you see repeated GET /api/notifications requests. Each one sits pending for up to ~25 seconds, then completes with fresh data (or an empty body), and a new identical request fires immediately after. What technique is this, and what is the server doing?#
Options
Show answer
Requests that hang for seconds and are replaced the instant they return are long polling: the server holds each request open until it has data (or a timeout elapses), and the client re-requests immediately after every response. That is why one request sits pending for seconds, then completes and is instantly replaced. Short polling instead completes immediately on a fixed interval, and a WebSocket would show a single upgraded connection rather than a stream of separate HTTP requests.
Long polling inverts ordinary request/response timing: the client sends a normal HTTP request, but the server deliberately does not answer until it has something to deliver (or a server-side timeout fires, returning an empty response). The moment a response arrives, the client issues the next request, so there is almost always one request "parked" on the server — that's the pending-for-seconds-then-instantly-replaced pattern in the network tab. Short polling looks different: requests complete immediately and recur on a fixed interval, wasting round trips when nothing has changed. A WebSocket would appear as a single upgraded connection, not a stream of separate HTTP requests. Long polling remains a useful fallback where WebSockets or SSE are blocked by an intermediary, because every hop sees only ordinary HTTP requests.
Web Platform/browser-apis/popover-api
Which behaviors does an element with popover (i.e. popover="auto") get from the browser for free, that a plain absolutely-positioned <div> does not?#
Options
Pick every one that applies.
Show answer
An auto popover gets three things for free: promotion to the top layer so it paints above everything regardless of z-index or overflow, light dismiss on outside click or Esc, and a one-at-a-time stack where opening an unrelated auto popover closes it. What it deliberately does not do is trap focus or make the page inert — popovers are non-modal; for that you need <dialog>.showModal().
An auto popover renders in the top layer (escaping z-index/overflow/clipping traps that plague hand-rolled menus), gets light-dismiss for free (outside click or Esc), and participates in a one-at-a-time stack so opening an unrelated auto popover closes the others. What it deliberately does NOT do is trap focus or make the background inert — popovers are non-modal by design. If you need a modal that blocks the rest of the page, that's <dialog>.showModal(), not a popover.
Web Platform/browser-apis/popover-api
A popover shown via the Popover API can still be clipped by an ancestor's overflow: hidden or hidden behind another element with a higher z-index.#
Options
Show answer
False. A shown popover is promoted to the top layer — a browser-managed layer above the entire document, outside every stacking context and clipping ancestor — so it can't be clipped by an ancestor's overflow: hidden or hidden behind a higher z-index. That is exactly the problem it solves for tooltips, menus, and dropdowns that ordinary positioned <div>s would lose.
When shown, a popover is promoted to the top layer — a browser-managed layer that sits above the entire normal document, outside every stacking context and clipping ancestor. That is exactly the problem it solves: tooltips, menus, and dropdowns built with ordinary positioned <div>s get clipped by overflow: hidden parents or lose z-index wars, while a top-layer popover (like a modal <dialog>) is immune to both.
Web Platform/browser-apis/fetch-api
Assume the endpoint returns the JSON body { "count": 5 }. What does this log?#
fetch('/api/data')
.then((res) => res.json())
.then((data) => console.log(data.count))
.catch((err) => console.log('error'));Show answer
5
fetch resolves with a Response; res.json() returns a promise that resolves to the parsed body, so the second .then receives { count: 5 } and logs data.count, which is 5. The .catch only runs on a network failure or a JSON parse error, neither of which occurs here.
Web Platform/browser-apis/fetch-api
The endpoint /api/report takes several seconds to respond. What does this log?#
const controller = new AbortController();
fetch('/api/report', { signal: controller.signal })
.then((res) => console.log(res.status))
.catch((err) => console.log(err.name));
controller.abort();Show answer
AbortError
Passing the controller's signal to fetch ties the request's lifetime to the controller. controller.abort() runs synchronously right after the request starts — long before the slow endpoint responds — so the fetch promise rejects and the .then never runs. The rejection reason is a DOMException whose name is "AbortError", so the .catch logs AbortError. This is the standard way to cancel in-flight requests (e.g. abandoning a stale search-as-you-type request); note that abort produces a rejection, not a resolved response, so it flows through the same path as a network failure.
Web Platform/browser-apis/popover-api
What is the difference between popover="auto" and popover="manual"?#
Show answer
auto (the default, also what bare popover means) gets light dismiss — an outside click or Esc closes it — and lives in a one-at-a-time stack, so opening an unrelated auto popover closes the others. manual opts out of all of that: it has no light dismiss and is not auto-closed by other popovers, so you must explicitly close it (button, hidePopover(), or togglePopover()); multiple manual popovers can be open at once. Use manual for things like toasts or persistent side panels.
The choice is about dismissal semantics. auto suits menus, dropdowns, and tooltips that should close when the user clicks away; manual suits UI that must stay put until the code or the user explicitly dismisses it. Both render in the top layer either way.
Web Platform/browser-apis/realtime-transports
Select all statements that are true when comparing a native WebSocket connection to EventSource (Server-Sent Events).#
Options
Pick every one that applies.
Show answer
Three of these are true: WebSocket is full-duplex while SSE is server-to-client only; WebSocket natively supports binary frames while SSE is text-only and needs a base64 detour for binary data; and EventSource reconnects automatically while a plain WebSocket requires the application to detect the close event and reconnect itself. The proxy claim is backwards — SSE's plain HTTP passes through more intermediaries unmodified than WebSocket's Upgrade handshake, which some proxies block.
WebSocket and SSE differ on three real axes: duplex direction (WebSocket both ways, SSE server-to-client only), payload type (WebSocket binary-capable, SSE text-only), and reconnection (SSE's EventSource retries automatically, WebSocket leaves it entirely to your code). The proxy claim is backwards: SSE, being ordinary long-lived HTTP, passes through more intermediaries unmodified, while WebSocket's Upgrade handshake is exactly the kind of thing some corporate proxies and older infrastructure block — one real reason to keep SSE or long polling as a fallback.
Web Platform/browser-apis/web-workers
This is meant to hand a worker a reference it can read text from. It throws immediately instead. What's the bug?#
const worker = new Worker('worker.js');
const el = document.getElementById('source');
worker.postMessage(el);Options
Show answer
postMessage uses the structured clone algorithm, which cannot clone a DOM node — this throws a DataCloneError synchronously; send the node's data (e.g. el.textContent) instead
Structured clone — the algorithm postMessage uses to copy data across the thread boundary — has an explicit list of cloneable types, and DOM nodes aren't on it (neither are functions). Passing one throws a synchronous DataCloneError immediately, before any message is sent. There's no way to hand a live DOM node to a worker at all, since workers have no DOM access regardless; the fix is always to extract the data you actually need (text, attribute values, a serializable object) and send that instead. The other options describe failures this code doesn't have: getElementById finding the element is assumed by the scenario, postMessage is routinely called from the main thread to a worker, and relative worker URLs are the normal case.
Web Platform/browser-apis/service-workers
Order the stages a new service worker version goes through, from registering it to it controlling pages, in the default case (no skipWaiting/clients.claim).#
Put these in order
Show answer
A new service worker version goes through these stages, in order:
register()fetches the worker script and creates a registration- The new worker enters
installingand runs itsinstallhandler (e.g. precaching) - Once installed, it enters
waitingif an old worker still controls open pages - Once no page is controlled by the old worker, it activates and runs its
activatehandler - Pages loaded after activation are controlled by the new worker
This is why a plain refresh doesn't pick up a new version — the outgoing page still controls via the old worker during the reload, so the new one stays in waiting unless the app forces through with skipWaiting()/clients.claim().
Each stage gates the next: a worker can't install before it's registered, can't activate while an old worker still controls a page (that's what waiting protects against — no mixing old and new logic on one open page), and doesn't control anything until activation completes. This is exactly why refreshing a tab isn't enough to pick up a new service worker version — the outgoing page is still 'controlled' by the old worker during that very reload, so the new one stays in waiting unless the app calls skipWaiting()/clients.claim() to force through.
Web Platform/browser-apis/web-workers
Which statement about a dedicated Web Worker is true?#
Options
Show answer
A dedicated Web Worker runs on a separate thread and cannot access the DOM or window. Because it runs in the background, long computations there don't block the UI. It communicates with the page only through postMessage and the message event, with data copied via structured clone or transferred.
A Web Worker runs script on a background thread, so long computations don't block the UI. It has no access to the DOM, window, or document; it communicates with the page only via postMessage and the message event, and data is copied (structured clone) or transferred.
Web Platform/browser-apis/view-transitions
Wrapping a same-document DOM update in document.startViewTransition(() => updateDOM()) will silently break the update in browsers that don't support the View Transitions API.#
Options
Show answer
False — but only if you guard the call. document.startViewTransition is undefined in unsupporting browsers, so calling it unconditionally throws. The standard pattern feature-detects first and falls back to calling updateDOM() directly, so the DOM still updates everywhere, just without the animation. That is progressive enhancement: the transition is a visual upgrade, not a requirement for the content to change.
It does not break, but you must guard it: document.startViewTransition is simply undefined in unsupporting browsers, so calling it throws. The standard pattern feature-detects — if (!document.startViewTransition) { updateDOM(); return; } document.startViewTransition(updateDOM); — so the DOM still updates everywhere, just without the animation where it isn't supported. That's progressive enhancement: the transition is a visual upgrade, never a requirement for the content to change.
Web Platform/browser-apis/view-transitions
This is meant to animate a same-document update with the View Transitions API, but no transition ever plays — the new content just snaps in. Which line is the problem?#
function showDetail(html) {
panel.innerHTML = html;
document.startViewTransition(() => {});
}Show answer
The bug is on line 2.
The DOM is mutated on line 2, before startViewTransition runs, so by the time the API captures its 'old' snapshot the panel already shows the new content — old and new states are identical and there is nothing to animate. The mutation must happen inside the callback: document.startViewTransition(() => { panel.innerHTML = html; });. The empty callback on line 3 does no work at all.
Web Platform/browser-apis/service-workers
A page requests /api/data and a registered service worker serves the response from cache instead of the network. Walk through end-to-end how that happens: what must be true of the page first, what runs inside the worker, and what happens if the worker's handler decides not to respond.#
Show answer
The page must be controlled by an active service worker whose scope covers it — the very first load after registration typically is not controlled (the worker only takes over pages loaded after it activates), unless the worker calls clients.claim(). Once the page is controlled, every request it makes in the worker's scope fires a fetch event inside the worker. The handler calls event.respondWith(...), typically with caches.match(request) and falling back to fetch(request) on a cache miss (often putting the fresh response into the cache). If the handler doesn't call respondWith, the browser simply lets the request proceed to the network as normal.
The key precondition trips people up: registering a worker does nothing to the page that registered it until that page is controlled, which normally only happens on the next navigation after the worker activates (or immediately, if the worker calls clients.claim()). From then on, requests within the worker's scope dispatch a fetch event in the worker; the handler answers with event.respondWith(...) — commonly caches.match(request) with a fetch(request) fallback. Not calling respondWith is a deliberate escape hatch: the request falls through to the network exactly as if no worker existed.
Web Platform/browser-apis/web-workers
How does a dedicated Web Worker communicate with the page, and how is the data passed?#
Show answer
Both sides use postMessage(data) to send and listen for the message event to receive. By default the data is deep-copied via the structured clone algorithm (functions and DOM nodes can't be sent); large ArrayBuffers can instead be transferred (zero-copy, but unusable on the sender afterwards).
Workers share no memory with the main thread, so all communication is message-passing. Structured clone copies most data; the transferable-objects optimization hands ownership of a buffer to the other side without copying.
Web Platform/browser-apis/view-transitions
In the View Transitions API, what does giving two elements the same view-transition-name achieve, and what mechanism animates them?#
Show answer
A shared view-transition-name tells the browser those are the same conceptual element across the old and new states, so instead of cross-fading it morphs one into the other — tweening position, size, and appearance (a thumbnail growing into a hero image, say). Mechanically, the browser snapshots each named element into its own ::view-transition-group(name) tree (::view-transition-old/::view-transition-new), and the default group animation interpolates between them; you can override it with CSS on those pseudo-elements.
Unnamed content gets a single root cross-fade; named elements are lifted into their own transition groups so the browser can animate them independently. Names must be unique among elements present at a given snapshot, which is why you often assign them dynamically to just the pair being morphed.
Web Platform/browser-apis/view-transitions
You call document.startViewTransition(updateDOM) for a same-document transition. Order the steps the browser performs.#
Put these in order
Show answer
The browser performs these steps in order:
- Capture a snapshot of the current (old) visual state
- Run the
updateDOMcallback, mutating the DOM to the new state - Capture a snapshot of the new state and build the
::view-transitionpseudo-element tree - Animate the old snapshots to the new ones (cross-fade, plus morphing any named groups)
This sequence is exactly why the DOM mutation must live inside the callback — otherwise there is no old state left to animate from.
The sequence is the whole reason the DOM mutation must live in the callback: old snapshot → callback mutates the DOM → new snapshot → animate between them. The ViewTransition object exposes this as promises — updateCallbackDone resolves after the callback, ready once the pseudo-element tree is built and the animation is about to run, and finished when it completes.
Web Platform/browser-apis/observers
What is logged, in order?#
const observer = new MutationObserver(() => console.log('observed'));
observer.observe(document.body, { childList: true });
console.log('1');
document.body.appendChild(document.createElement('div'));
console.log('2');Show answer
1
2
observed
MutationObserver callbacks are delivered as microtasks, batched: the observer queues a record when the mutation happens, but the callback doesn't run until the current synchronous script finishes and the microtask queue drains. So the synchronous console.log('1') and console.log('2') both run first — the appendChild in between only queues a mutation record, it doesn't invoke the callback inline — and 'observed' logs last, after the script completes. This is a deliberate change from the old, removed Mutation Events API, which fired synchronously per-mutation and could recursively trigger more mutations mid-handler; batching into one microtask callback with an array of records avoids that entirely.
Web Platform/browser-apis/storage-apis
You need to add a new object store to an existing IndexedDB database. Where does that schema-change code have to live, and what makes it run?#
Show answer
You call indexedDB.open('shop', newVersionNumber) with a version number higher than the database's current version. That triggers an upgradeneeded event on the open request, and the handler for that event is the only place createObjectStore/createIndex are legal — the event carries a special versionchange transaction that wraps the schema change. Trying to create an object store outside onupgradeneeded (e.g. after the connection's onsuccess fires) throws an InvalidStateError, because by then you're in a normal transaction that isn't allowed to alter the database's structure.
IndexedDB treats schema changes as a distinct, versioned event: opening with a higher version number is the only trigger for upgradeneeded, and the versionchange transaction it provides is the only context where the database structure can change. This is why IndexedDB code always tracks and bumps a version number rather than checking 'does this store exist yet' at runtime — the browser enforces the schema-change boundary for you.
Web Platform/browser-apis/view-transitions
Why does the same-document View Transitions API require you to make the DOM change inside the callback passed to startViewTransition, rather than updating the DOM yourself and then calling it?#
Show answer
Because the API needs to capture both the before and after states, and the callback is its hook for the moment in between. When you call startViewTransition(cb), the browser first snapshots the current (old) visual state, then runs your callback to mutate the DOM, then snapshots the new state, and animates from old to new. If you mutated the DOM first and only then called startViewTransition, the 'old' snapshot would already be the new layout — there'd be nothing to animate from, so you'd get no transition (or a broken one). Putting the mutation in the callback is what lets the browser bracket it with the two snapshots.
The callback is a synchronization point, not just a convenience. The browser must own the timing so it can sandwich the DOM mutation between an old-state capture and a new-state capture; surrendering that ordering (mutating outside) destroys the 'from' state the animation interpolates out of. This is also why the callback can return a promise — the browser waits for the new state to settle before capturing it.
Sources
The official documentation these questions are checked against:
Related interview questions
Job market
See web-platform salaries and hiring demand from live job postings.
The other 17 questions
This page shows 25 and marks what you pick. That's as far as a page can go. A free account opens the other 17 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan