Production Incidents • Releases • Async Design • Browser Performance • Security • Code Review • 2026

JavaScript Interview Questions for Experienced Candidates (5 Years)

JavaScript interviews for 5 years of experience stop asking how a feature works and start asking why you chose something and what it cost. Expect questions on deploys that broke open tabs, data bugs, async design, browser performance, security calls, and stories about reviews and mentoring. It is written for JavaScript developers with roughly five to seven years behind them who own a module or a part of the front end, make design calls inside it, get pulled in when it breaks and review other people's code. Each answer is a first-person story or a decision you can defend. Swap in your own project details before you say it out loud.

Search all questions by round, difficulty and level, or save the ones you want to practice.

Shipping and Releases 3 questions

Hard Behavioral round Mid-level, Senior Practice question

1. After each deploy, some users got a chunk load error when they opened a lazy-loaded page. What caused it, and how did you fix it for good?

What the interviewer is really testing:
Whether you understand how hashed file names, open tabs and caching interact during a deploy, and fix it in the deploy rather than only catching the error.
Answer frame:

Cause: an open tab still runs the old bundle, which asks for old chunk files the deploy deleted.

Deploy fix: keep the previous builds' files for a while, and never cache the HTML page for long.

Safety net: catch a failed dynamic import and reload once, with a guard so it can't loop.

Sample spoken answer:

“Our build gave every chunk a hashed file name, and each deploy replaced the whole assets folder. Anyone who had the app open was still running the old main bundle, so when they clicked into a lazy-loaded page, it asked for an old chunk that no longer existed and got a 404. The error spikes lined up exactly with deploy times, which is how I found it. The real fix was in the deploy: we kept the previous two builds' files on the server, so old tabs could still load what they asked for. We also made sure the HTML page was never cached for long, while hashed files were cached for a year. As a safety net, a failed dynamic import now triggers one full reload, with a flag so it can't loop. The cost was a bit more storage on the server.”

Code:
async function loadPage(importer) {
  try {
    const mod = await importer();
    sessionStorage.removeItem('chunk-reload');
    return mod;
  } catch (err) {
    if (!sessionStorage.getItem('chunk-reload')) {
      sessionStorage.setItem('chunk-reload', '1');
      location.reload(); // pick up the new build, once
    }
    throw err;
  }
}

const reports = await loadPage(() => import('./reports.js'));
Red flag to avoid:

Telling users to clear their cache, or retrying the same import, which asks for the same missing file again.

They may ask next:
  • Why is it fine to cache hashed files for a long time but not the HTML page?
  • How would you tell users a new version is ready instead of reloading under them?
Say it in 60 seconds
Hard Behavioral round Mid-level, Senior Practice question

2. You added a service worker for offline support, and after a release some users kept seeing the old version for days. What was going on, and how did you fix updates?

What the interviewer is really testing:
Whether you know the service worker life cycle, especially that a new worker waits while old tabs are open, and can design an update flow users understand.
Answer frame:

Cause: the new worker installs, then waits until no tab is using the old one.

Update flow: spot the waiting worker, tell the user, and activate it when they agree.

Hygiene: a cache name per release, old caches deleted on activate.

Sample spoken answer:

“We cached the app shell with a service worker so the app opened offline. After one release, support heard from people still on the old version days later. The browser had found and installed the new worker, but a new worker waits until no tab is using the old one. These users never closed their tab; they just refreshed it, and a refresh doesn't count, because the old worker keeps control. I didn't want to call skipWaiting on every install, because swapping workers under a running page can mix old code with newly cached files. So the page now checks for a waiting worker and shows a small 'New version, refresh' banner. When the user clicks it, we tell the worker to skip waiting and reload once it takes control. Each release also gets its own cache name. The cost is that some people stay on the old version until they click.”

Code:
// page
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (!refreshing) { refreshing = true; location.reload(); }
});
navigator.serviceWorker.register('/sw.js').then((reg) => {
  const offer = (w) => showUpdateBanner(() => w.postMessage('SKIP_WAITING'));
  if (reg.waiting) offer(reg.waiting);
  reg.addEventListener('updatefound', () => {
    const w = reg.installing;
    w.addEventListener('statechange', () => {
      if (w.state === 'installed' && navigator.serviceWorker.controller) offer(w);
    });
  });
});

// sw.js
self.addEventListener('message', (event) => {
  if (event.data === 'SKIP_WAITING') self.skipWaiting();
});
Red flag to avoid:

Telling users to clear site data, or not knowing that a new worker waits while old tabs are still open.

They may ask next:
  • Why is calling skipWaiting on every install risky for a page that's already running?
  • How would you get users off a broken service worker you've already shipped?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

3. Your team wants to stop transpiling for old browsers and drop most polyfills to shrink the bundle. How do you decide it's safe, and how do you roll it out?

What the interviewer is really testing:
Whether you make a support decision from your own users' data, know what changing build targets really changes, and plan for the people you might break.
Answer frame:

Data: which browsers your real users and key customers use, not global charts.

Measure: change the build's browser targets and compare what you actually save.

Safety: a plain 'please update' message, errors watched by browser, a quick way back.

Sample spoken answer:

“I'd start with our own analytics, not general usage charts, and look at who's on the old browsers. If it's a tiny slice but includes a big customer's locked-down office machines, that changes the answer. Then I'd change the build's browser targets and compare bundle sizes, so we know what we'd really save. It's often a lot, because old targets force extra helper code and large polyfills. For rollout, I'd add a tiny separate script, written in old syntax, that shows a 'please update your browser' message. It can't live inside the main bundle, because new syntax in an old browser is a parse error and none of that file runs. After release I'd watch error monitoring split by browser for a week. At my last company the saving was worth it, and we kept a one-line switch to rebuild with the old targets if support tickets jumped.”

Red flag to avoid:

Deciding from global browser share or your own laptop, with no plan for the users who will now see a blank page.

They may ask next:
  • Why can't the 'please update' check live inside the main bundle?
  • How would you handle a big customer who can't upgrade for another year?
Say it in 60 seconds

Reliability 2 questions

Hard Behavioral round Mid-level, Senior Practice question

4. You added memoization to speed up a hot function, and weeks later the app's memory kept climbing. What went wrong, and how did you bound the cache?

What the interviewer is really testing:
Whether you understand that every cache is a memory decision, and can choose between WeakMap and a size-limited cache for the key type you have.
Answer frame:

Cause: an unbounded Map holding keys and results that are never removed.

Object keys: a WeakMap lets entries go when the key object is no longer used.

Other keys: a size-limited LRU cache, and accept the recompute when an entry is evicted.

Sample spoken answer:

“We memoized a function that built formatted views of report configs, using a plain Map keyed by the config object. It made the page fast, but on a dashboard people left open all day, memory crept up until the tab crashed. A heap snapshot showed the Map holding thousands of old configs that nothing else used, because the Map itself kept them alive. For that case I switched to a WeakMap, so an entry disappears once the config object is gone. Another memoized function took string keys, which a WeakMap can't hold, so I wrote a small LRU on top of Map insertion order and capped it. The trade-off is that a WeakMap can't tell you its size, and an LRU sometimes recomputes something it threw away, which we measured as cheap.”

Code:
function lruMemo(fn, max = 500) {
  const cache = new Map();
  return (key) => {
    if (cache.has(key)) {
      const value = cache.get(key);
      cache.delete(key);
      cache.set(key, value); // move to most recently used
      return value;
    }
    const value = fn(key);
    cache.set(key, value);
    if (cache.size > max) cache.delete(cache.keys().next().value); // oldest
    return value;
  };
}
Red flag to avoid:

Adding a cache with no limit and no eviction, and treating memory as free.

They may ask next:
  • Why can't a WeakMap be iterated or report its size?
  • How would you pick the size limit for the LRU?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

5. Your front-end error dashboard was so noisy that a real checkout bug went unnoticed for a week. How did you make the error reports useful again?

What the interviewer is really testing:
Whether you own front-end monitoring: cut noise you can't fix, make real errors readable, and alert on changes rather than totals.
Answer frame:

Cut noise: drop errors that come only from browser extensions, and group the rest properly.

Make readable: upload source maps, tag errors with the release, fix 'Script error.' at the source.

Alert on change: new error types after a deploy, not raw totals.

Sample spoken answer:

“We had thousands of errors a day, mostly from browser extensions injecting scripts, bots and a few harmless browser warnings, so everyone had learned to ignore the dashboard. A broken payment button sat in there for a week. I spent a couple of days cleaning it up. We filtered out errors whose stack pointed only at extension files and grouped the rest properly. A big bucket was just 'Script error.' with no details, which is what the browser reports for errors in scripts from another origin. We added the crossorigin attribute and the matching CORS header on our CDN, and real stack traces appeared. We uploaded source maps to the error tool without serving them publicly, and tagged every error with the release. Then alerts fired on new error types after a deploy. The cost is keeping the filter rules up to date, but people trust the dashboard again.”

Red flag to avoid:

Muting the alerts because they're noisy, or working through the error list by count with no idea which ones hurt users.

They may ask next:
  • Why does the browser hide the details of errors from scripts on another origin?
  • How do you decide which errors deserve an alert and which only a weekly look?
Say it in 60 seconds

Data Correctness 2 questions

Hard Behavioral round Mid-level, Senior Practice question

6. Support reported that clicking one record sometimes opened a different one. The cause turned out to be the IDs themselves. What was happening, and how did you fix it?

What the interviewer is really testing:
Whether you know JavaScript numbers lose precision above the safe integer limit, and whether you fixed it at the API contract rather than patching one screen.
Answer frame:

Cause: 64-bit IDs parsed as numbers lose digits above Number.MAX_SAFE_INTEGER.

Fix: send IDs as strings and treat them as opaque, never as numbers.

Why not BigInt: JSON.parse still makes a rounded number, and JSON.stringify throws on BigInt.

Sample spoken answer:

“The backend had moved to 64-bit IDs, and once they grew past about nine quadrillion, JSON.parse turned them into JavaScript numbers that couldn't hold every digit. Two different IDs could round to the same number, so we fetched the wrong record. I proved it in the console by parsing the raw response and comparing it with the text. I looked at BigInt, but a normal JSON.parse reviver only sees the number after it's already been rounded, and JSON.stringify throws on BigInt values, so it would have meant custom parsing everywhere. Instead I agreed with the backend team to send IDs as strings, kept the old numeric field for one release so older clients didn't break, and added a review rule that IDs are never used in arithmetic. It cost a coordinated release, but it removed the whole problem.”

Code:
JSON.parse('{"id": 9007199254740993}').id; // 9007199254740992, one off
Number.isSafeInteger(9007199254740993);        // false
JSON.parse('{"id": "9007199254740993"}').id; // exact, as a string
Red flag to avoid:

Calling it a random backend bug, or trying to fix it by rounding or with parseInt on the client.

They may ask next:
  • What exactly is Number.MAX_SAFE_INTEGER, and why that value?
  • How would you roll out a change to an API field that other clients depend on?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

7. Users with the app open in two tabs edited the same record and silently overwrote their own changes. How did you keep tabs in sync, and how did you stop lost updates?

What the interviewer is really testing:
Whether you see two problems here, keeping tabs informed and preventing lost writes, and know a browser tool for the first and a server-side check for the second.
Answer frame:

Tell other tabs: a BroadcastChannel message, or the storage event, when data changes.

Stop lost writes: send a version with every save and let the server reject a stale one.

Conflicts: show the user what changed and let them choose, rather than silently merging.

Sample spoken answer:

“Each tab loaded the record once and kept its own copy, so a save in one tab quietly replaced what the other had saved a minute earlier. I treated it as two problems. To keep tabs informed, after a successful save we post a message on a BroadcastChannel, and any other tab showing that record refetches it or shows a 'changed in another tab' note. That alone isn't safe, though: a tab can miss the message, and the same user might be on a second device. So the real fix was the save itself. Every record has a version, the client sends it back in an If-Match header, and the server answers 412 if someone saved first. The client then shows both versions and lets the user pick. The cost was a version field in the API and a conflict screen, but lost edits stopped.”

Code:
const channel = new BroadcastChannel('records');

async function save(record) {
  const res = await fetch(`/api/records/${record.id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json', 'If-Match': record.etag },
    body: JSON.stringify(record.data),
  });
  if (res.status === 412) return showConflict(record.id); // someone saved first
  if (!res.ok) throw new Error(`Save failed: ${res.status}`);
  channel.postMessage({ type: 'saved', id: record.id });
}

channel.onmessage = ({ data }) => {
  if (data.type === 'saved' && data.id === currentId) refetchRecord(data.id);
};
Red flag to avoid:

Fixing only the tab sync and assuming the last save should always win.

They may ask next:
  • Why doesn't BroadcastChannel on its own prevent lost updates?
  • When would you merge changes automatically instead of asking the user?
Say it in 60 seconds

Async Design 3 questions

Medium Behavioral round Mid-level, Senior Practice question

8. A dashboard that polled for updates overloaded the API whenever responses got slow, and again when laptops woke from sleep. What was wrong, and how did you fix it?

What the interviewer is really testing:
Whether you know setInterval doesn't wait for async work, and can design polling that never overlaps, backs off on errors and pauses in the background.
Answer frame:

Cause: setInterval started a new request on schedule whether the last one had finished or not.

Fix: schedule the next poll only after the current one ends, and back off on errors.

Background: stop while the tab is hidden, fetch once when it's visible again.

Sample spoken answer:

“The dashboard used setInterval to fetch every five seconds. That's fine while responses take a few hundred milliseconds, but when the backend slowed down, each request took longer than five seconds, so they piled up on top of each other and made the backend slower still. Hidden tabs kept polling too, and when laptops woke up, lots of dashboards fired at the same moment. I replaced it with a loop that waits for the request to finish and only then schedules the next one with setTimeout, so each tab has at most one request in flight. On errors the delay doubles up to a minute. When the page is hidden we stop, and when it becomes visible we fetch once and carry on. The cost was slightly staler data in background tabs, which nobody was looking at anyway.”

Code:
let delay = 5000;
let timer = null;
let running = false;

async function poll() {
  if (running) return;
  running = true;
  clearTimeout(timer);
  try {
    render(await getJson('/api/dashboard'));
    delay = 5000;
  } catch {
    delay = Math.min(delay * 2, 60000); // back off while the API struggles
  } finally {
    running = false;
  }
  if (!document.hidden) timer = setTimeout(poll, delay);
}

document.addEventListener('visibilitychange', () => {
  clearTimeout(timer);
  if (!document.hidden) poll();
});
poll();
Red flag to avoid:

Just making the interval longer, which still stacks requests up whenever the server is slow.

They may ask next:
  • Why did laptops waking from sleep make it worse?
  • At what point would you move from polling to a pushed stream of updates?
Say it in 60 seconds
Medium Coding round Mid-level, Senior Practice question

9. Several parts of the page call initSdk on load, and it must only run once. Write a helper that shares one call between them, and lets a later caller retry if it failed.

What the interviewer is really testing:
Whether you know to cache the promise rather than the result, and have thought about what a cached failure does to the rest of the page.
Answer frame:

Cache the promise: store it on the first call, so callers who arrive mid-flight share it.

On failure: clear it, so the next caller tries again instead of getting the old error for ever.

Trade-off: callers already waiting all get the same rejection.

Sample spoken answer:

“The trick is to cache the promise, not the result. If I only stored the result, callers arriving before the first call finished would each start their own, which is exactly how we once loaded an SDK three times. So the first call starts the work and stores the promise straight away, and every later caller gets that same promise, whether it's still running or already done. The other half is failure. If I kept a rejected promise, the page would be stuck with that error even after the network came back. So when it rejects, I clear the stored promise, and the next caller starts a fresh attempt. Callers already waiting on the failed one still get the error, which I think is right, because each of them can decide whether to retry.”

Code:
function onceAsync(fn) {
  let pending = null;
  return (...args) => {
    if (!pending) {
      pending = Promise.resolve()
        .then(() => fn(...args))
        .catch((err) => {
          pending = null; // let the next caller try again
          throw err;
        });
    }
    return pending;
  };
}

const initSdk = onceAsync(() => loadScript('/sdk.js').then(() => window.Sdk.init()));
await Promise.all([initSdk(), initSdk()]); // the script loads once
Red flag to avoid:

Using a done flag instead of a stored promise, so callers who arrive during the first call either start it again or get nothing back.

They may ask next:
  • Later calls ignore their own arguments here. When is that a problem?
  • How would you add a timeout so a first call that hangs doesn't block everyone?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

10. Have you used an async generator in real code, for example to page through an API? Why that over a normal loop, and what caught you out?

What the interviewer is really testing:
Whether you know async iteration from real use: lazy fetching, early exit and cleanup, and that it runs one step at a time.
Answer frame:

Why: callers loop over items with for await and never see cursors or pages.

Early exit: breaking out stops further fetches and runs any finally block.

Catch: it's sequential by design, so it's the wrong tool when pages could load in parallel.

Sample spoken answer:

“We had cursor-based pagination repeated in several places, each with its own loop and bugs around the last page. I wrapped it in one async generator that fetches a page, yields its items, and follows the cursor until there isn't one. Callers just write for await over orders. The nice part was laziness: a caller looking for the first matching order can break, and no more pages are fetched. What caught me out was cleanup. Breaking out calls the generator's return, so a finally block runs, which is where I released a lock, and a teammate's version without finally leaked it. The other trade-off is that it's strictly one page at a time. For a bulk export where speed mattered, I used a concurrency-limited fetch instead.”

Code:
async function* allOrders(fetchPage) {
  let cursor = null;
  do {
    const page = await fetchPage(cursor);
    yield* page.items;
    cursor = page.nextCursor;
  } while (cursor);
}

for await (const order of allOrders(getOrdersPage)) {
  if (order.flagged) { review(order); break; } // stops fetching more pages
}
Red flag to avoid:

Collecting every page into one big array first, then looping, which defeats the point and can blow up memory.

They may ask next:
  • What happens if the consumer's loop body throws?
  • How would you add a small prefetch of the next page without losing the simple interface?
Say it in 60 seconds

Design Choices 4 questions

Hard Technical round Mid-level, Senior Practice question

11. Other teams are going to depend on a JavaScript module you own. What design choices do you make in its public API, and how do you change it later without breaking them?

What the interviewer is really testing:
Whether you design for callers you don't control: a small surface, predictable async behaviour, and a real plan for versions and deprecation.
Answer frame:

Surface: few exports, an options object instead of long positional arguments.

Predictable: a function is always async or always sync, never sometimes each.

Change: semantic versions, deprecation warnings first, an exports map to hide internals.

Sample spoken answer:

“When I built our shared form-validation package, I kept the public surface small and gave functions an options object, so adding a setting later didn't break anyone's argument order. Anything that might ever need the network returned a promise every time, even when a result was cached, because code that's sometimes sync and sometimes async causes ordering bugs that are painful to trace. I listed entry points in the package exports map, so other teams couldn't import our internal files. That choice cost us: two teams already imported internals, so adding the map was a major version, and I helped them move. After that we followed semantic versions strictly, logged a deprecation warning for at least one minor release before removing anything, and kept a changelog that said what to change, not just what changed.”

Code:
// package.json: only these paths can be imported by other teams
{
  "name": "@acme/validation",
  "exports": {
    ".": "./dist/index.js",
    "./rules": "./dist/rules.js"
  }
}
Red flag to avoid:

Exporting everything for convenience and renaming functions in a minor release.

They may ask next:
  • What goes wrong when a function calls its callback synchronously sometimes and asynchronously other times?
  • How do you find out who uses a deprecated function before you remove it?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

12. Users lost long form drafts when their session expired, so you started saving drafts in the browser. How did you choose between localStorage and IndexedDB, and what did it cost?

What the interviewer is really testing:
Whether you choose browser storage from its real limits, such as sync versus async, size and data types, and also think about privacy and eviction.
Answer frame:

localStorage: simple, but synchronous, strings only and small, so big writes block the page.

IndexedDB: asynchronous, stores objects and files, but the raw API is awkward.

Costs: shared computers, clearing on logout, and storage the browser may evict.

Sample spoken answer:

“The drafts were long and some had attached images, so localStorage was out. It only stores strings, it's small, and every write blocks the main thread, which we'd feel when saving a big draft every few seconds while someone types. I chose IndexedDB behind a tiny wrapper with get, set and delete, so the rest of the code never touched its event-based API. It stores objects and Blobs directly, and writes don't block the page. Then the less technical costs. On a shared computer the next person could read a draft, so we clear drafts on logout and never keep fields marked sensitive. The browser can also evict storage when space runs low, so the draft is a safety net, not the only copy, and we still save to the server while the session is valid. I also debounced saves so we weren't writing on every keystroke.”

Code:
function openDb() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open('drafts', 1);
    req.onupgradeneeded = () => req.result.createObjectStore('drafts');
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function saveDraft(formId, draft) {
  const db = await openDb();
  return new Promise((resolve, reject) => {
    const tx = db.transaction('drafts', 'readwrite');
    tx.objectStore('drafts').put(draft, formId);
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}
Red flag to avoid:

Putting everything into localStorage as JSON without thinking about size, blocking the page or who else uses that computer.

They may ask next:
  • Why does a synchronous API like localStorage matter for how the page feels?
  • How would you tell users a draft was restored, and let them throw it away?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

13. You built a widget that other sites embed on pages you don't control. Why did you choose, or avoid, a web component with Shadow DOM, and what did that cost?

What the interviewer is really testing:
Whether you can weigh style isolation against the real costs of Shadow DOM, and compare it with an iframe instead of picking what's in fashion.
Answer frame:

Problem: host pages' global CSS kept breaking the widget.

Options: an iframe isolates everything but is clunky; Shadow DOM isolates styles and stays in the page.

Costs: fonts and theming need planning, and id links like a label's for don't cross the boundary.

Sample spoken answer:

“Our booking widget went onto customers' sites with one script tag, and their global CSS kept breaking it, like a site-wide rule restyling every button. An iframe would have isolated everything, but sizing it to its content and passing events across was clunky, and it felt like a separate page. So I built it as a custom element with a shadow root. Page styles stopped leaking in, apart from inherited things like font and colour, which we actually wanted. The costs showed up later. Custom fonts declared only inside the shadow root didn't load reliably, so we declared them in the host document. Customers wanted to theme it, so we exposed CSS custom properties and a few named parts, which became a public API we had to keep stable. And a label outside couldn't point at an input inside by id. I'd choose it again, but plan theming from day one.”

Red flag to avoid:

Claiming Shadow DOM isolates everything, including inherited styles, or naming no cost at all.

They may ask next:
  • How do CSS custom properties let the host page theme a component inside a shadow root?
  • When would an iframe be the better choice after all?
Say it in 60 seconds
Hard System design round Mid-level, Senior Practice question

14. You needed live order status on a dashboard. How did you choose between polling, server-sent events and WebSockets, and what did your choice cost?

What the interviewer is really testing:
Whether you pick a real-time approach from the actual data flow, not habit, and know the operational costs of each one.
Answer frame:

Direction: updates only flow server to browser, or both ways?

Options: polling is simplest, SSE suits one-way streams, WebSockets suit two-way.

Costs: reconnects, proxies and load balancers, connection limits, and missed events.

Sample spoken answer:

“Updates only flowed from server to browser, a few times a minute per order, so I ruled out WebSockets: two-way connections and their own protocol weren't needed. Polling every few seconds would have worked, but with many open dashboards it was a lot of empty requests. I chose server-sent events. It's plain HTTP, EventSource reconnects by itself, and sending an event id lets the server replay what a client missed after a drop. The costs were real, though. Our proxy buffered responses, so events arrived in clumps until we turned buffering off for that route. Over HTTP/1.1, each open stream uses one of the few connections a browser allows per site, which hurt people with many tabs until we served it over HTTP/2. And we kept a slow poll as a fallback for networks that cut long-lived connections.”

Code:
const source = new EventSource('/api/orders/stream');
source.addEventListener('status', (e) => {
  const { id, status } = JSON.parse(e.data);
  updateRow(id, status);
});
source.onerror = () => showStaleBanner(); // EventSource retries by itself
Red flag to avoid:

Choosing WebSockets by default for everything, without mentioning reconnects, proxies or missed events.

They may ask next:
  • How would you avoid missing events that happen while a client is reconnecting?
  • What would change your answer to WebSockets?
Say it in 60 seconds

Browser Performance 2 questions

Medium Technical round Mid-level, Senior Practice question

15. A card grid janked every time it resized, and the profiler kept flagging forced reflow. What causes that in JavaScript, and how did you fix it?

What the interviewer is really testing:
Whether you know that reading layout after writing styles forces the browser to recalculate layout synchronously, and how to batch reads and writes.
Answer frame:

Cause: a loop that writes a style and then reads a size forces layout every pass.

Fix: do all the reads first, then all the writes, or schedule writes together.

Better still: let CSS do it when grid or flex can handle the layout.

Sample spoken answer:

“We had code that equalised card heights on resize. For each card it read the height of its content and then set the card's style height. Each write invalidated layout, so the next read forced the browser to recalculate layout right there, once per card, and with a few hundred cards that meant dropped frames. The performance panel showed a stack of purple layout blocks with a forced reflow warning pointing at our loop. I split it into two passes: read every height first into an array, then write every style. That turned hundreds of layouts into one. Later I went further and replaced most of it with CSS grid, which equalises rows without any JavaScript. The trade-off of the two-pass version is holding the array, which is trivial next to the layout cost.”

Code:
const cards = [...document.querySelectorAll('.card')];

// Before: write, then read, every pass forces a layout
cards.forEach((card) => {
  card.style.height = `${card.firstElementChild.offsetHeight}px`;
});

// After: all reads, then all writes
const heights = cards.map((card) => card.firstElementChild.offsetHeight);
cards.forEach((card, i) => { card.style.height = `${heights[i]}px`; });
Red flag to avoid:

Blaming the framework or adding debounce without understanding that reads and writes are interleaved.

They may ask next:
  • Which properties and methods force a synchronous layout when you read them?
  • Where does requestAnimationFrame help with this, and where doesn't it?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

16. Real-user data showed clicks on a filter panel felt slow, even though it seemed instant on your laptop. How did you find the slow part and fix it?

What the interviewer is really testing:
Whether you trust field data over your own machine, know a click feels slow when the main thread is busy before the next paint, and can split urgent work from the rest.
Answer frame:

Measure: field data on interaction delay, then reproduce with CPU throttling in the profiler.

Find: the handler filtered, sorted and re-rendered before the browser could paint anything.

Fix: show the visible change first, yield to the browser, then do the heavy work.

Sample spoken answer:

“Our real-user monitoring tracked Interaction to Next Paint, and the filter panel was our worst screen, mostly on mid-range phones. On my laptop it felt instant, so I turned on CPU throttling in the performance panel and recorded a click. The handler did everything in one go: updated the selected chip, filtered and sorted a few thousand rows, rebuilt the table and saved the choice to localStorage. The browser couldn't paint the selected chip until all of that finished. I split it. The handler now updates the chip and shows a small 'updating' hint straight away, yields to the browser with a zero timeout, and only then filters and renders just the visible rows. Saving the preference moved to the end. The trade-off is a brief moment where the chip changed but the table hasn't yet, which users didn't mind, and our field numbers came well under target.”

Code:
const yieldToMain = () => new Promise((resolve) => setTimeout(resolve, 0));

filterPanel.addEventListener('change', async (event) => {
  markSelected(event.target); // cheap, and visible right away
  showUpdatingHint();
  await yieldToMain(); // let the browser paint first
  const rows = applyFilters(allRows, readFilters());
  renderVisibleRows(rows);
  savePreferences(readFilters());
});
Red flag to avoid:

Testing only on a fast laptop and deciding the field data must be wrong.

They may ask next:
  • Why doesn't marking the handler async, on its own, make it feel any faster?
  • When would you move the filtering into a Web Worker instead?
Say it in 60 seconds

Security 4 questions

Hard Technical round Mid-level, Senior Practice question

17. Your page and an embedded iframe from another domain talk to each other with postMessage. What did you check to make that safe in both directions?

What the interviewer is really testing:
Whether you know postMessage crosses the same-origin boundary on purpose, so the sender must name the target origin and the receiver must check who sent the message.
Answer frame:

Sending: pass the exact target origin, never '*' for anything that matters.

Receiving: check event.origin against an allow list, and event.source against the frame you expect.

Data: treat every message as untrusted input, and confirm anything important on the server.

Sample spoken answer:

“We embedded a payment-details widget hosted on a partner's domain, and the two sides exchanged messages about size and status. In review I found two gaps. Our page sent messages with '*' as the target origin, so if that frame had been navigated somewhere else, the other page would have received our data. I changed it to the partner's exact origin. And our listener acted on any message with the right type field, so any window holding a reference to ours, like a page that opened us, could send a fake 'payment complete'. Now the listener ignores anything whose origin isn't on our allow list or whose source isn't that iframe, and it validates the message shape before using it. We also never trust a success message alone; the server confirms the payment. The cost was an allow list we keep per environment.”

Code:
const WIDGET_ORIGIN = 'https://widget.partner.example';
const frame = document.querySelector('#payment-widget');

frame.contentWindow.postMessage({ type: 'init', orderId }, WIDGET_ORIGIN);

window.addEventListener('message', (event) => {
  if (event.origin !== WIDGET_ORIGIN) return;
  if (event.source !== frame.contentWindow) return;
  const msg = event.data;
  if (msg?.type === 'resize' && Number.isFinite(msg.height)) {
    frame.style.height = `${msg.height}px`;
  }
});
Red flag to avoid:

Sending with '*' as the target origin and trusting any message that has the right type field.

They may ask next:
  • Why check event.source as well as event.origin?
  • What does the sandbox attribute on the iframe add?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

18. A security report says a request body can set a property on every object in your service through a deep merge helper. How does that work, and how do you fix it?

What the interviewer is really testing:
Whether you understand prototype pollution through __proto__ keys and can fix both the helper and the design that allowed it.
Answer frame:

How: JSON.parse keeps __proto__ as a normal key, and a naive merge then writes into Object.prototype.

Fix the helper: skip __proto__, constructor and prototype, and only recurse into own properties.

Fix the design: validate input shape, and use Map or Object.create(null) for dictionaries.

Sample spoken answer:

“Our settings endpoint deep-merged the request body into a defaults object. JSON.parse keeps a key called __proto__ as an ordinary own property, so when the merge helper read that key on the target, it actually got Object.prototype and copied the attacker's fields onto it. After that, every plain object in the process had, say, an isAdmin property, and a check like if user.isAdmin passed for everyone. I fixed the helper to skip __proto__, constructor and prototype and to only recurse into properties the target owns. More importantly, the endpoint now validates the body against a schema that allows only known keys, and lookup tables keyed by user input became Map objects. I also added a test that sends the payload and checks a fresh object stays clean. The trade-off was rejecting a few unknown keys old clients sent.”

Code:
function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
    const val = source[key];
    if (val && typeof val === 'object' && !Array.isArray(val)) {
      const cur = Object.hasOwn(target, key) ? target[key] : null;
      target[key] = cur && typeof cur === 'object' ? cur : {};
      safeMerge(target[key], val);
    } else {
      target[key] = val;
    }
  }
  return target;
}
Red flag to avoid:

Thinking it only matters in the browser, or fixing it by blocking the literal string in the raw request body.

They may ask next:
  • Why does Object.create(null) protect a lookup table from this?
  • What does freezing Object.prototype buy you, and what can it break?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

19. On release day, a critical security alert lands for a package deep in your dependency tree. How do you decide whether to hold the release, and what do you do next?

What the interviewer is really testing:
Whether you judge real exposure instead of panicking or ignoring alerts, and know how lockfiles and overrides let you fix a transitive dependency.
Answer frame:

Exposure: is it shipped to users or servers, or only used by build and test tools?

Fix path: update the parent package, or pin the child with overrides in package.json.

Compromised package: check if the bad version was ever installed, and rotate secrets it could see.

Sample spoken answer:

“First I'd check what the package does for us. I'd trace who pulls it in, and whether it ends up in the code we ship or only runs in the build or tests. A regex denial-of-service in a test tool is not the same as one in our request parser. If it ships and the vulnerable path is reachable, I'd hold the release. The fix is usually updating the parent package, and if the parent hasn't released yet, pinning the child to a patched version with overrides, then running the full test suite because we've forced a version the parent never tested. If the alert is a hijacked package rather than a bug, it's more serious: I'd check the lockfile history and CI logs for whether that version was ever installed, and rotate any tokens our CI exposes. Either way, I'd write down the decision so the next alert is quicker.”

Code:
// package.json: force a patched version of a nested dependency
{
  "overrides": {
    "vulnerable-lib": "^2.4.1"
  }
}
Red flag to avoid:

Either running a forced audit fix that bumps majors on release day, or ignoring the alert because the app still works.

They may ask next:
  • Why does installing from the lockfile in CI matter here?
  • How would you keep the dependency tree smaller so this happens less often?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

20. Marketing asks you to add another third-party script to the checkout page before a campaign. What do you check before you agree, and what might you push back on?

What the interviewer is really testing:
Whether you treat a third-party script as code with full page access and a performance cost, and can negotiate rather than just block.
Answer frame:

Access: a script on the page can read the whole DOM, including form fields.

Cost: load it async or deferred, measure its main-thread time, set a budget.

Control: Content Security Policy, consent, and a way to switch it off fast.

Sample spoken answer:

“I'd start by reminding everyone that a script on checkout can read every field on that page, so it's a security decision as well as a marketing one. I'd ask what it needs to measure. Often the thank-you page is enough, and it has no form fields. If it truly must be on checkout, I'd make sure it loads async so it can't block rendering, measure how much main-thread time it takes on a mid-range phone, and check it against our performance budget. It would go into our Content Security Policy explicitly, load only after the user's consent where that's required, and sit behind a switch we can turn off without a deploy. At my last company that conversation moved the tag to the confirmation page, marketing still got their numbers, and checkout stayed clean.”

Red flag to avoid:

Pasting the snippet into the page head because the request came from above.

They may ask next:
  • What does subresource integrity protect against, and why is it hard to use with many vendor scripts?
  • How would you notice if a vendor script suddenly got much slower?
Say it in 60 seconds

Team and Code Quality 3 questions

Medium Situational round Mid-level, Senior Practice question

21. A teammate's pull request replaces direct function calls between modules with a global event bus, 'so everything is decoupled'. How do you review it?

What the interviewer is really testing:
Whether you can weigh a design change in review, name its real costs in JavaScript, and disagree without shutting the person down.
Answer frame:

Understand: ask what problem it solves today, not in theory.

Costs: calls you can't trace, misspelled event names that fail silently, listeners that leak.

Middle ground: direct calls inside a feature, events only where many listeners really exist.

Sample spoken answer:

“I'd start by asking what pain it fixes, because 'decoupled' sometimes just means the dependency is hidden. Then I'd be specific about costs. With a direct call, I can jump to the function and my editor finds every caller. With string event names, a typo means the event fires and nobody listens, and there's no error. The order listeners run in becomes a hidden rule, an error in one listener can get swallowed, and any component that subscribes but forgets to unsubscribe leaks. If there's a real case, like several unrelated widgets that must react to a user logging out, I'd suggest events just for that, with names as constants in one file and a subscribe helper that returns an unsubscribe function. I'd talk it through on a short call rather than leave twenty comments, and approve the smaller version.”

Red flag to avoid:

Approving it because it looks cleaner, or rejecting it with 'we don't do that here' and no reasons.

They may ask next:
  • How would you catch a misspelled event name before it reaches production?
  • When is an event bus genuinely the better design?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

22. Tell me about a time you brought in a linter or formatter to a team that argued about style in every review. How did you get people to accept it?

What the interviewer is really testing:
Whether you can lead a small team-wide change: agree the rules, make it cheap to adopt, and handle the noisy parts without forcing it.
Answer frame:

Agree: a short discussion to pick defaults, not a debate over every rule.

Adopt: format the codebase in one commit and keep that commit out of blame.

Enforce gently: run it in CI and on commit, and turn off rules that only make noise.

Sample spoken answer:

“Reviews on our team kept stalling on semicolons, quotes and line breaks, which wasted everyone's time. I proposed a formatter plus a linter focused on bugs, not taste. Instead of debating every rule, I suggested we take the formatter's defaults and spend one meeting only on the handful of lint rules that caused real disputes. I reformatted the whole repo in a single commit on a quiet afternoon and added it to the blame ignore file so history stayed useful. Then CI checked it, and a pre-commit hook fixed things automatically. A couple of rules produced lots of warnings with no real bugs behind them, so we turned those off after two weeks. Style comments in review basically disappeared, and the cost was one big diff and some merge pain for open branches that day.”

Red flag to avoid:

Pushing a strict rule set with hundreds of errors on everyone's open branches without asking anyone.

They may ask next:
  • How would you handle one senior engineer who strongly disagreed with a rule?
  • Which lint rules actually catch bugs, as opposed to style?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

23. A junior has spent two days on a bug where this is undefined inside a callback. Walk me through how you helped them without just fixing it yourself.

What the interviewer is really testing:
Whether you can mentor through a real JavaScript bug: guide them to find it, explain the rule behind it, and build their confidence.
Answer frame:

Guide: ask them to show what they tried, then set a breakpoint together.

Explain: a method passed as a callback loses its this, because this depends on how it's called.

Follow up: let them pick the fix and check in on the next similar bug.

Sample spoken answer:

“I sat with them and asked them to walk me through what they'd tried, which already showed me they were changing things at random. I suggested a breakpoint inside the method, and when it hit, I asked what this was. It was undefined, and then I asked how the method was being called. They'd passed this.handleSave straight to an event helper, so it was called as a plain function, not as a method. I explained the rule in one sentence: this is decided by how a function is called, not where it's written. Then I let them pick a fix, and they chose an arrow wrapper at the call site. It took longer than me just saying the answer, but a week later they found a similar bug on their own and told me about it afterwards.”

Red flag to avoid:

Taking the keyboard and fixing it silently, or leaving them alone because they should work it out.

They may ask next:
  • How do you decide when to step in and when to let someone keep struggling?
  • How do you make it safe for juniors to ask for help earlier?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

You practiced these. On the real call, ClapAssist helps with the rest.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.

Download with 10 free minutes
Mac and Windows · Stays out of screen share · No card