Everyday Bugs • Async at Work • Testing • Browser Work • Tooling • Owning Features • 2026

JavaScript Interview Questions for 3 Years Experience (2 to 4 Years)

JavaScript interviews for 3 years of experience care less about definitions and more about what happened in your code: a date that showed the wrong day, a fetch call that failed quietly, a test that only broke in the full suite, a page that froze. It is written for JavaScript developers with about two to four years of real work, the stage where you own features end to end but someone else still designs the system. Each question shows what the interviewer is checking, the shape of a good answer and a short spoken answer. Swap the stories for your own before the interview.

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

Data and Dates 3 questions

Medium Technical round Mid-level Practice question

1. A cart total showed a long tail of decimals, like 0.30000000000000004, and a rounded total was off by one cent. How did you handle prices after that?

What the interviewer is really testing:
Whether you know JavaScript numbers are binary floating point and have a real habit for exact amounts, not just a toFixed call at the end.
Answer frame:

Cause: every number is a 64-bit float, so values like 0.1 and 0.2 are stored slightly off and the error shows up after adding.

Fix: keep amounts as whole numbers in the smallest unit and only divide when you display.

Display: format with Intl.NumberFormat, and be careful with toFixed, which returns a string and rounds the stored binary value.

Sample spoken answer:

“Every JavaScript number is a 64-bit float, so 0.1 and 0.2 can't be stored exactly, and adding them gives 0.30000000000000004. On one project we summed item prices as decimals and rounded at the end with toFixed, and some totals were off by a cent. toFixed rounds the binary value, which is why 1.005 toFixed 2 gives 1.00, not 1.01. The fix was to keep every amount as a whole number in the smallest unit, so integer maths stays exact, and only divide by 100 when formatting. We used Intl.NumberFormat for the display so the symbol and separators matched the user's locale. We also made the API send amounts as integers, so the front end never had to guess.”

Code:
// amounts stored as whole minor units, never as decimals
const totalMinor = cart.reduce((sum, item) => sum + item.priceMinor * item.qty, 0);

const formatter = new Intl.NumberFormat(userLocale, { style: 'currency', currency: currencyCode });
const shown = formatter.format(totalMinor / 100);
Red flag to avoid:

Calling it a JavaScript bug, or fixing it only by adding toFixed(2) at the end of every calculation.

They may ask next:
  • Where does integer maths stop being exact in JavaScript, and when would that matter?
  • How would you split a total three ways so the parts still add up exactly?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

2. Users in some time zones saw a due date one day earlier than the one saved. The code did new Date('2026-03-05'). What went wrong, and what did you change?

What the interviewer is really testing:
Whether you know how date strings are parsed and keep calendar dates apart from moments in time.
Answer frame:

Cause: a date-only ISO string is parsed as midnight UTC, while a date-time string with no offset is parsed as local time.

Symptom: displayed in a time zone behind UTC, midnight UTC is still the previous evening, so the day slips back.

Fix: keep calendar dates as plain strings or build them in local time, and send real moments as ISO strings with an offset.

Sample spoken answer:

“The string '2026-03-05' is a date-only ISO string, and JavaScript parses that as midnight UTC. When we formatted it for users in a time zone behind UTC, midnight UTC was still the evening of March 4 for them, so the date showed a day early. The confusing part is that '2026-03-05T00:00' without an offset is parsed as local time, so two strings that look almost the same behave differently. A due date is a calendar date, not a moment in time, so I stopped turning it into a Date at all. We kept it as the plain string for storage and display, and when we needed date maths we built it with new Date(year, month - 1, day), which is local. Real timestamps, like when something was created, we send as full ISO strings with a Z.”

Red flag to avoid:

Fixing it by adding or subtracting hours by hand, which breaks again for another time zone or at a daylight saving change.

They may ask next:
  • How would you write a test that catches this even though your laptop is in one time zone?
  • How do you show a timestamp in the user's time zone rather than the server's?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

3. You saved a user's settings object in localStorage. After a reload, the last-login date is a string and a Map inside it is empty. What happened?

What the interviewer is really testing:
Whether you know localStorage only holds strings and what JSON.stringify silently changes or drops.
Answer frame:

Strings only: localStorage stores strings, so objects go through JSON.stringify and JSON.parse.

What JSON loses: Dates become ISO strings, Maps and Sets become empty objects, undefined and functions are dropped, and BigInt throws.

Fix: convert those fields on the way in and out, keep a version number, and wrap writes in try/catch.

Sample spoken answer:

“localStorage only stores strings, so we saved settings with JSON.stringify and read them back with JSON.parse. JSON has no Date type, so the Date was written as an ISO string and came back as a plain string, and the code calling getTime on it crashed. The Map was worse: JSON.stringify turns a Map into an empty object, so the user's saved filters just disappeared. Undefined fields and functions get dropped too, and a BigInt makes stringify throw. My fix was a small pair of functions that turn the settings into a plain shape, with the date as a string and the Map as an array of pairs, and rebuild the real types when loading. I also stored a version number so old saved data could be migrated, and wrapped every storage call in try/catch, because it throws when storage is full or blocked.”

Red flag to avoid:

Expecting localStorage to store objects as they are, or trusting whatever comes back from JSON.parse without checking its shape.

They may ask next:
  • How would you migrate settings saved by an older version of the app?
  • When would you choose IndexedDB over localStorage?
Say it in 60 seconds

Everyday JavaScript 3 questions

Medium Coding round Mid-level Practice question

4. A product list sorted by price put 100 before 20, and names starting with an accented letter landed at the end. Why, and how do you fix both sorts?

What the interviewer is really testing:
Whether you know that the default sort compares strings by code unit, and how to sort numbers and human text properly.
Answer frame:

Default sort: with no compare function, sort turns values into strings and compares UTF-16 code units, so '100' comes before '20'.

Numbers: pass a compare function that subtracts.

Text: use localeCompare or an Intl.Collator so accents and case sort the way people expect.

Sample spoken answer:

“When you call sort with no compare function, JavaScript converts every element to a string and compares code units. So 100 becomes '100', and '1' is less than '2', which puts 100 before 20. For prices I pass a compare function that returns a minus b. The names problem is the same default: an accented capital like É has a higher code unit than every plain letter, so it lands after Z, and capitals sort before lowercase. For text I use an Intl.Collator, which knows the rules for the user's language, and I create it once rather than calling localeCompare inside every comparison on a big list. And I use toSorted, so the list I was given stays in its original order.”

Code:
[100, 20, 3].sort();                // [100, 20, 3], compared as strings

const byPrice = products.toSorted((a, b) => a.price - b.price);

const collator = new Intl.Collator(userLocale, { sensitivity: 'base' });
const byName = products.toSorted((a, b) => collator.compare(a.name, b.name));
Red flag to avoid:

Not knowing why the numbers came out in that order, or writing a compare function that returns true and false.

They may ask next:
  • How would you sort by category first, then by price within each category?
  • What does the numeric option on a collator do for names like 'item 2' and 'item 10'?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

5. Searching for 'R&D' or 'C#' returned the wrong results, because the code built the request URL by joining strings. What went wrong, and how do you build URLs now?

What the interviewer is really testing:
Whether you know which characters break a URL and reach for the built-in URL tools instead of gluing user text into a string.
Answer frame:

Cause: an ampersand starts a new parameter, a hash starts the fragment that is never sent to the server, and a plus sign is often read as a space.

Fix: build the URL with the URL class and set values through searchParams, which escapes each one.

Single values: encodeURIComponent works too, but encodeURI is the wrong tool because it leaves ampersands and hashes alone.

Sample spoken answer:

“The old code did '/api/search?q=' plus whatever the user typed. For 'R&D', the ampersand ends the q parameter, so the server saw q as just R and a stray parameter called D. For 'C#', the hash starts the fragment, and the browser never sends the fragment to the server, so it searched for C. A plus sign had its own problem, because many servers read it as a space. After that I stopped gluing user text into URLs by hand. I build them with the URL class and set each value through searchParams, which escapes it properly, and fetch accepts the URL object directly. If I only need to escape one value, encodeURIComponent is fine, but encodeURI is the trap, because it leaves ampersands and hashes as they are.”

Code:
const url = new URL('/api/search', window.location.origin);
url.searchParams.set('q', query);          // 'R&D' and 'C#' arrive intact
url.searchParams.set('page', String(page));
const res = await fetch(url);
Red flag to avoid:

Building URLs by joining strings and patching only the one character that broke.

They may ask next:
  • How would you read the page's own query string to restore a user's filters after a reload?
  • Why is it risky to put an email address or a token in a URL?
Say it in 60 seconds
Hard Technical round Mid-level Practice question

6. A word counter built on a plain object gave a strange string as the count for the word 'constructor'. Why, and how would you rewrite it?

What the interviewer is really testing:
Whether you see that plain objects inherit keys from Object.prototype and know when a Map or a prototype-free object is the right choice.
Answer frame:

Cause: a plain object inherits properties, so counts['constructor'] already holds the Object function before you set anything.

Result: the function plus one becomes string concatenation, and a key like '__proto__' is ignored or worse.

Fix: use a Map for data keyed by user input, or Object.create(null), and check own keys with Object.hasOwn.

Sample spoken answer:

“The code did counts[word] = (counts[word] || 0) + 1. For most words the lookup is undefined, so it starts at zero. But a plain object inherits from Object.prototype, and that has a constructor property pointing at the Object function. So counts['constructor'] was already truthy, and the function plus one turned into a long string ending in 1. The word '__proto__' was worse: assigning a string to it is silently ignored, so that count just vanished. Any time the keys come from users or outside data, I use a Map now. It has no inherited keys, any value can be a key, and it has a proper size and get and set. If I need a plain object, say for JSON, I create it with Object.create(null).”

Code:
const counts = new Map();
for (const word of words) {
  counts.set(word, (counts.get(word) ?? 0) + 1);
}
counts.get('constructor'); // a real number now
Red flag to avoid:

Saying 'constructor' is a reserved word, or not knowing that objects inherit properties at all.

They may ask next:
  • What is prototype pollution, and how can merging user JSON into an object cause it?
  • When would you still choose a plain object over a Map?
Say it in 60 seconds

Async Work 4 questions

Medium Coding round Mid-level Practice question

7. A page showed a blank table instead of an error when the API returned 500, and a slow request hung forever. Write the fetch wrapper you would use.

What the interviewer is really testing:
Whether you know fetch only rejects on network failure and has no timeout of its own, and whether you handle both in one place.
Answer frame:

HTTP errors: fetch resolves on a 404 or 500, so check response.ok and throw yourself.

Timeouts: there is no default timeout, so pass an abort signal.

One place: a small wrapper that every call uses, with the status attached to the error for the caller.

Sample spoken answer:

“The surprise with fetch is that a 404 or a 500 is not a rejection. The promise resolves as long as a response came back, so our code happily called json on an error body and rendered an empty table. It only rejects when the request can't complete, like a network failure or an abort. So I wrote one wrapper: it checks response.ok and throws an error that carries the status, so callers can treat a 404 differently from a 500. For the hanging request, fetch has no timeout, so I pass AbortSignal.timeout, which aborts the request and rejects with a TimeoutError. Every screen then used this wrapper, and the table showed a proper error message with a retry button instead of looking empty.”

Code:
async function getJson(url, { timeoutMs = 8000 } = {}) {
  const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
  if (!res.ok) {
    const err = new Error('Request failed with status ' + res.status);
    err.status = res.status;
    throw err;
  }
  return res.json();
}
Red flag to avoid:

Believing a .catch on fetch will catch a 500, or having no idea how to stop a request that never answers.

They may ask next:
  • How would you let the caller cancel the request as well, for example when the user leaves the page?
  • What happens in this wrapper if the server answers 204 with no body?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

8. Code calls items.forEach(async item => await save(item)) and then shows 'All saved'. Users see the message before saving ends, and failures never show. Why?

What the interviewer is really testing:
Whether you know forEach ignores the promises its callback returns, and how to pick between running saves one by one or together.
Answer frame:

Cause: forEach calls the async callback and throws away the promise, so nothing waits.

Errors: a rejection inside becomes an unhandled rejection, and a try/catch around forEach never sees it.

Fix: for...of with await to go one at a time, or await Promise.all over map to run them together.

Sample spoken answer:

“forEach doesn't know anything about promises. It calls the async callback for each item, each call returns a promise, and forEach just discards them. So the loop finishes straight away and 'All saved' shows while every save is still in flight. The failures were lost for the same reason: a rejected promise nobody is holding becomes an unhandled rejection, and the try/catch around the forEach had already finished. The fix depends on what you need. If order matters, or the server can't take many requests at once, I use a for...of loop with await, which runs them one after another. If they're independent, I map the items to promises and await Promise.all, and then the catch really does see a failure. We also added a lint rule that flags async callbacks passed to forEach.”

Code:
// one after another
for (const item of items) {
  await save(item);
}

// all together, and a failure reaches the catch
await Promise.all(items.map(item => save(item)));
showMessage('All saved');
Red flag to avoid:

Adding await in front of forEach and expecting it to wait.

They may ask next:
  • With Promise.all, how would you tell the user which items failed and which were saved?
  • Do map and filter have the same problem with async callbacks?
Say it in 60 seconds
Hard Coding round Mid-level Practice question

9. A user drops 300 files to upload, and firing all the requests at once swamps the browser and the server. Write a helper that runs at most five at a time.

What the interviewer is really testing:
Whether you can control async work yourself, keep results in order, and think about what happens when one task fails.
Answer frame:

Workers: start a fixed number of runners that each pull the next item from a shared index.

Order: write each result at its own index, so the output matches the input.

Failure: decide whether one failure stops everything or is recorded per item.

Sample spoken answer:

“I start a fixed number of runners, five here, and they share one index. Each runner takes the next item, awaits the upload and loops until nothing is left. Taking the index and moving it on happens in one synchronous step, and JavaScript runs one piece of code at a time, so two runners can never grab the same file. I write each result at the item's own position, so the results come back in input order even though uploads finish in any order. Then I await Promise.all on the runners. As written, one failed upload rejects the whole thing, though the other runners keep going. For uploads I actually catch inside the worker and record success or failure per file, so the user sees which ones to retry.”

Code:
async function mapWithLimit(items, limit, worker) {
  const results = new Array(items.length);
  let next = 0;

  async function runner() {
    while (next < items.length) {
      const i = next++;
      results[i] = await worker(items[i], i);
    }
  }

  const count = Math.min(limit, items.length);
  await Promise.all(Array.from({ length: count }, () => runner()));
  return results;
}
Red flag to avoid:

Splitting the files into fixed batches of five and waiting for each whole batch, without noticing one slow file holds up the other four.

They may ask next:
  • How would you add a progress bar that updates as each upload finishes?
  • How would you let the user cancel the remaining uploads?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

10. A partner API fails now and then with 503s and timeouts. Write a retry helper, and tell me which calls you would never retry automatically.

What the interviewer is really testing:
Whether you retry only what is safe to retry, back off with jitter, and stop at a limit.
Answer frame:

What to retry: timeouts, network errors, 5xx and 429, never a 400 or a 401.

How: a capped number of attempts with a growing wait and a bit of randomness.

Safety: only repeat calls that are safe to repeat, such as reads or writes with an idempotency key.

Sample spoken answer:

“I wrap the call in a loop with a maximum number of attempts. If it fails, I first ask whether retrying could help. A timeout, a network error, a 5xx or a 429 might pass next time. A 400 or a 401 will fail the same way forever, so I throw straight away. Between attempts I wait longer each time and add some randomness, so a hundred browsers that failed together don't all hit the server again at the same moment. The part I'd stress in an interview is which calls to retry. Reading data is safe. Creating an order or charging a card isn't, unless the API accepts an idempotency key so a repeated request is recognised. Without that, a retry after a timeout can create the same order twice.”

Code:
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

function isRetriable(err) {
  if (err.name === 'TimeoutError') return true;
  if (err.status === undefined) return err.name === 'TypeError'; // network failure
  return err.status === 429 || err.status >= 500;
}

async function withRetry(fn, attempts = 3, baseMs = 300) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt >= attempts || !isRetriable(err)) throw err;
      await sleep(baseMs * 2 ** (attempt - 1) + Math.random() * baseMs);
    }
  }
}
Red flag to avoid:

Retrying every error in a tight loop with no wait and no limit, including requests that create or charge something.

They may ask next:
  • The API sends a Retry-After header with a 429. How would you use it?
  • How would you stop retrying altogether if the partner API is down for an hour?
Say it in 60 seconds

Testing 2 questions

Medium Technical round Mid-level Practice question

11. How do you unit test code that waits 30 seconds before showing a warning, or code that calls fetch? Show me with Jest or Vitest.

What the interviewer is really testing:
Whether you write fast, deterministic tests by controlling time and the network instead of really waiting or really calling the API.
Answer frame:

Timers: switch to fake timers and move the clock forward by hand.

Network: mock fetch, or the small module that wraps it, and return the response shape you need.

Check behaviour: assert on what the user would see or what was called, not on internals.

Sample spoken answer:

“For the timer, I don't want a test that really waits 30 seconds, so I turn on fake timers. Then I advance the clock to just before 30 seconds and check the warning hasn't fired, advance one more millisecond and check it has. That also proves the boundary. For fetch, I replace it with a mock that resolves to an object with ok and a json method, so I can test the success path, a 500 and a thrown network error without a server. In a real codebase I'd rather mock our own API module than fetch itself, because then the tests don't care how the request is built. And I reset mocks and restore real timers after each test, so one test can't leak into the next.”

Code:
afterEach(() => {
  jest.useRealTimers();
  jest.restoreAllMocks();
});

test('warns after 30 seconds idle', () => {
  jest.useFakeTimers();
  const onIdle = jest.fn();
  startIdleTimer(onIdle, 30000);
  jest.advanceTimersByTime(29999);
  expect(onIdle).not.toHaveBeenCalled();
  jest.advanceTimersByTime(1);
  expect(onIdle).toHaveBeenCalledTimes(1);
});

test('loads the user name', async () => {
  jest.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({ name: 'Asha' }),
  });
  await expect(loadUserName(7)).resolves.toBe('Asha');
});
Red flag to avoid:

Using real waits in tests, or tests that call the real API and fail whenever it is slow.

They may ask next:
  • How would you test the error message shown when the request fails?
  • When does mocking too much make a test worthless?
Say it in 60 seconds
Hard Situational round Mid-level Practice question

12. A test passes when you run it on its own with .only, but fails when the whole test file runs. How do you find out why?

What the interviewer is really testing:
Whether you recognise leaking state between tests and can narrow it down methodically instead of adding retries.
Answer frame:

Suspect shared state: module-level caches, mocks that were never restored, fake timers left on, leftover DOM.

Narrow it down: run it with only the test before it, then halve the list until you find the one that breaks it.

Fix the leak: reset in afterEach and make each test set up what it needs.

Sample spoken answer:

“If it passes alone and fails with its neighbours, some earlier test is leaving something behind. The usual suspects are a module-level cache or singleton that keeps data between tests, a mock that was set up but never restored, fake timers still switched on, or DOM nodes left in the test document. To find the culprit, I run the failing test together with just one earlier test at a time, or halve the file until it passes again. Last time it was a module that cached the logged-in user: one test logged in as an admin, and a later test expected a guest. The fix was a reset function for that cache called in beforeEach, plus restoring mocks after every test in the config. I avoid adding retries, because that hides the leak.”

Red flag to avoid:

Marking the test as skipped or adding automatic retries and moving on.

They may ask next:
  • How would you make it impossible for this kind of leak to come back?
  • What would you check if the test fails only on the CI server, never on your laptop?
Say it in 60 seconds

Debugging 2 questions

Medium Technical round Mid-level Practice question

13. Walk me through how you use the browser dev tools on a bug you can reproduce. What do you reach for beyond console.log?

What the interviewer is really testing:
Whether you debug with the tools efficiently, or only by scattering log lines and reloading.
Answer frame:

Network first: check the request and response, so you know whether it is a front-end or back-end problem.

Breakpoints: conditional breakpoints, logpoints and pausing on exceptions, then read the call stack and scope.

DOM and events: break on DOM changes and on event listeners when you don't know which code is touching an element.

Sample spoken answer:

“I start in the Network tab, because half the time the request itself is wrong or the response isn't what the code expects, and that tells me which side the bug is on. If the data is fine, I set a breakpoint in the Sources panel where the value first looks wrong and read the call stack and the scope, so I see how we got there. In a loop I use a conditional breakpoint, like only when the id matches the broken row, and logpoints when I want a log without editing code. Pause on exceptions is great when something is caught and swallowed. And when I don't know what's changing an element, I set a breakpoint on DOM subtree modifications, and it stops on the exact line that touched it.”

Red flag to avoid:

Only ever using console.log and page reloads, and never having used a breakpoint.

They may ask next:
  • How would you debug a click handler when you don't know which file registers it?
  • How do you debug a page that works on desktop but breaks on a phone?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

14. How do you structure error handling in code you own? Show how you add context to an error without losing the original one.

What the interviewer is really testing:
Whether you catch only where you can act, keep the original error for debugging, and give the user a useful message.
Answer frame:

Catch with a purpose: handle it, add context and rethrow, or let it go up.

Keep the chain: pass the original as cause, and use small Error subclasses for errors callers need to tell apart.

At the edge: one place shows a friendly message and logs the full error.

Sample spoken answer:

“My rule is to catch only where I can do something: recover, add context, or show the user a message. In between, I let errors go up. When I add context I don't throw away the original. I create a new Error with a clear message and pass the original as cause, so the log shows 'Could not load invoice 42' and underneath it the real network error. For errors callers need to react to, like not found or a validation failure, I use small subclasses with their own name, so the caller can check instanceof. Then at the edge, the route handler or the top of the screen, one piece of code decides what the user sees and sends the whole chain to our error logging. What I avoid is catch blocks that just log and carry on.”

Code:
class NotFoundError extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = 'NotFoundError';
  }
}

async function loadInvoice(id) {
  try {
    return await getJson('/api/invoices/' + id);
  } catch (err) {
    if (err.status === 404) throw new NotFoundError('No invoice ' + id, { cause: err });
    throw new Error('Could not load invoice ' + id, { cause: err });
  }
}
Red flag to avoid:

Catch blocks everywhere that log and continue, or rethrowing a new error with the original thrown away.

They may ask next:
  • Why might instanceof fail for an error that came from another window or iframe?
  • What should the user see, and what should only go to the logs?
Say it in 60 seconds

Browser Work 4 questions

Medium Technical round Mid-level Practice question

15. In code review you see comments rendered with element.innerHTML = comment.text. The author says the API is ours, so it's safe. What do you say?

What the interviewer is really testing:
Whether you recognise cross-site scripting through innerHTML and know the safe way to put user text on a page.
Answer frame:

Risk: the text came from a user, and markup like an image with an onerror attribute runs script when inserted.

Fix: textContent or creating elements, so the text is never parsed as HTML.

If HTML is needed: a proven sanitiser with an allow list, plus a Content Security Policy as a second layer.

Sample spoken answer:

“I'd say the API being ours doesn't matter, because the text inside it was typed by a user. If someone posts a comment with an image tag whose onerror attribute runs code, innerHTML parses it and that code runs in every reader's session, with access to whatever the page can reach. A script tag inserted through innerHTML won't run, which is why people think they're safe, but event handler attributes do. The fix is simple: set textContent, or build the elements yourself, so the comment is always treated as text. If we really need formatting like bold and links, we run it through a well-tested sanitiser with a short allow list of tags, rather than writing our own regex. And a Content Security Policy that blocks inline scripts gives us a second layer.”

Code:
const p = document.createElement('p');
p.textContent = comment.text; // shown as text, never parsed as HTML
list.append(p);
Red flag to avoid:

Agreeing it's safe because the data comes from our own server, or trying to block script tags with a regex.

They may ask next:
  • Is escaping on the server when the comment is saved enough on its own?
  • Where else in a front end can user input end up being run as code?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

16. QA finds that clicking Place order twice quickly creates two orders. How do you fix it on the front end, and why isn't that enough?

What the interviewer is really testing:
Whether you guard the submit properly on the client and also know the server has to protect itself.
Answer frame:

Client: listen to the form's submit event, set an in-flight flag and disable the button until the request settles.

Recover: re-enable it in finally, so an error doesn't leave the user stuck.

Server: an idempotency key or unique constraint, because a slow network, a retry or a second tab still gets through.

Sample spoken answer:

“On the front end I handle the form's submit event, not the button's click, because pressing Enter also submits. When a submit starts I set a flag and disable the button, and any submit while the flag is set is ignored. I clear it in a finally block, so if the request fails the user can try again instead of staring at a dead button. But that only stops the easy case. A user can double-submit from two tabs, the browser can retry, or our own retry logic can resend after a timeout. So I also generate an idempotency key when the checkout screen opens and send it with the request, and the back end refuses a second order with the same key. The front-end guard is for a nice experience; the server check is the real protection.”

Code:
let submitting = false;
form.addEventListener('submit', async (event) => {
  event.preventDefault();
  if (submitting) return;
  submitting = true;
  button.disabled = true;
  try {
    await placeOrder(readForm(form), idempotencyKey);
  } finally {
    submitting = false;
    button.disabled = false;
  }
});
Red flag to avoid:

Only disabling the button on click and calling it fixed, with no thought for the server side.

They may ask next:
  • What should the page do if the request times out and you don't know whether the order went through?
  • How would you show the user that something is happening while the order is in flight?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

17. An infinite-scroll feed used a scroll listener that checked positions on every scroll event, and it felt janky. How did you rebuild it?

What the interviewer is really testing:
Whether you know IntersectionObserver and the practical traps of loading more content as the user scrolls.
Answer frame:

Problem: scroll fires very often, and reading positions on each one forces layout work on the main thread.

Rebuild: an IntersectionObserver on a small marker at the end of the list, with a root margin to load early.

Traps: block overlapping loads, stop observing at the end, and handle the marker staying visible after a short page.

Sample spoken answer:

“The old code listened to scroll, and on every event it measured the list with getBoundingClientRect to see if we were near the bottom. Scroll fires constantly and each measurement can force layout, so it stuttered on slower phones. I replaced it with an IntersectionObserver watching an empty marker element after the last item. The browser tells us when the marker comes into view, and a root margin makes that happen a little before the user actually reaches the end, so the next page is ready. I kept a loading flag so we never fetch the same page twice, and stopped observing once the API said there were no more pages. One trap I hit: if a page is short and the marker stays visible, the observer doesn't fire again, so after each load I stop observing the marker and observe it again, which makes the browser check it once more.”

Code:
let loading = false;
const observer = new IntersectionObserver(async ([entry]) => {
  if (!entry.isIntersecting || loading) return;
  loading = true;
  try {
    const hasMore = await loadNextPage();
    observer.unobserve(marker);
    if (hasMore) observer.observe(marker); // checks again, fires if still in view
  } finally {
    loading = false;
  }
}, { rootMargin: '400px' });
observer.observe(marker);
Red flag to avoid:

Keeping the scroll listener and only adding more work to it, or not guarding against the same page loading twice.

They may ask next:
  • The feed grows to thousands of items and scrolling gets slow again. What would you do?
  • How do you keep the user's place when they open an item and come back?
Say it in 60 seconds
Hard Technical round Mid-level Practice question

18. Importing a large CSV file in the browser freezes the page for several seconds, and the spinner stops spinning. Why, and what did you do about it?

What the interviewer is really testing:
Whether you understand that long synchronous work blocks the main thread, and know the options for moving it off or breaking it up.
Answer frame:

Cause: parsing runs on the main thread, and while it runs the page can't paint or respond to input.

Move it off: a Web Worker does the parsing and posts the rows back.

Or break it up: process in chunks and yield between them, showing progress.

Sample spoken answer:

“JavaScript on a page runs on one main thread, and that thread also paints the screen and handles clicks. Our parser went through the whole file in one synchronous loop, so for those seconds nothing else could run, not even the spinner animation, which is why it froze. Making it async wouldn't help, because the work itself is still one long block. I moved the parsing into a Web Worker. The page posts the File to the worker, the worker reads and parses it, sends progress messages as it goes, and posts the rows back at the end. The main thread stays free, so the spinner turns and the user can cancel. The one cost is that data sent between them is copied, so for very large results I sent them back in batches.”

Code:
// main.js
const worker = new Worker(new URL('./parse-worker.js', import.meta.url), { type: 'module' });
worker.onmessage = (event) => {
  if (event.data.type === 'progress') showProgress(event.data.done);
  if (event.data.type === 'rows') renderRows(event.data.rows);
};
worker.postMessage(file); // a File can be sent straight to a worker

// parse-worker.js
self.onmessage = async (event) => {
  const text = await event.data.text();
  const rows = parseCsv(text, (done) => self.postMessage({ type: 'progress', done }));
  self.postMessage({ type: 'rows', rows });
};
Red flag to avoid:

Saying that wrapping the loop in a promise or an async function would stop the freeze.

They may ask next:
  • What can't a Web Worker do that the main thread can?
  • If you couldn't use a worker, how would you split the work so the page stays responsive?
Say it in 60 seconds

Tooling and Builds 2 questions

Medium Technical round Mid-level Practice question

19. The build passes on your laptop but fails on CI after a fresh install, with an error inside a dependency. Nobody changed that dependency. What do you check?

What the interviewer is really testing:
Whether you understand version ranges, lock files and environment differences well enough to make builds repeatable.
Answer frame:

Versions: a caret range lets a fresh install pull a newer minor or patch release than the one on your machine.

Lock file: commit it, and install on CI with npm ci, which installs exactly what it lists.

Environment: the same Node version everywhere, pinned in the repo.

Sample spoken answer:

“Nobody changed the dependency in package.json, but a range like caret 2.3.0 accepts any later 2.x release, so a fresh install can pull a newer version than the one I have. Or it's a dependency of a dependency, which package.json doesn't even list. The first thing I check is whether the lock file is committed and up to date, and whether CI uses npm install or npm ci. npm ci installs exactly what the lock file says and fails if the lock file and package.json disagree, which is what you want on a build server. I compare the installed versions on both machines to find the one that moved. The other usual cause is a different Node version on CI, so we pinned it in the repo and in the pipeline.”

Red flag to avoid:

Deleting node_modules and the lock file until it works, without finding what changed.

They may ask next:
  • How do you update dependencies on purpose without surprises?
  • A teammate keeps deleting the lock file when they get a merge conflict in it. What do you tell them?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

20. After you added a charting library used on one report page, the main bundle got much bigger and the home page loads slower. How do you fix it?

What the interviewer is really testing:
Whether you can find what is in a bundle and load heavy code only where it is used.
Answer frame:

Measure: run a bundle analyser to see what actually got added and where it ended up.

Split: load the library with a dynamic import on the page that needs it, so it becomes its own chunk.

Trim: import only what you use, and prefer libraries that ship ES modules the bundler can tree-shake.

Sample spoken answer:

“First I'd look rather than guess, so I run the bundle analyser for our build and see the library sitting in the main chunk that every page downloads. Since only the report page uses it, I load it with a dynamic import when that page opens. The bundler then puts it in a separate chunk, the home page never downloads it, and the report page shows a small loading state for a moment. I also check how we import it. Importing the whole package can pull in every chart type, while importing only the chart we use lets the bundler drop the rest, as long as the library ships ES modules without side effects. Last time that brought the main bundle back close to where it was, and I added a size check to CI so we'd notice next time.”

Code:
async function openReport() {
  const { renderSalesChart } = await import('./sales-chart.js'); // separate chunk
  renderSalesChart(document.querySelector('#chart'), reportData);
}
Red flag to avoid:

Not knowing how to see what is inside the bundle, or accepting a slower home page for one internal report.

They may ask next:
  • What does tree shaking need from a library to work?
  • How would you stop the report page feeling slow now that the chart loads on demand?
Say it in 60 seconds

Owning Features 3 questions

Medium Behavioral round Mid-level Practice question

21. Walk me through a front-end feature you owned end to end, from the ticket to users having it. What did you decide yourself?

What the interviewer is really testing:
Whether you really owned the work, including the unclear parts, testing and rollout, or only wrote the code you were handed.
Answer frame:

Start: what the ticket asked, and what you clarified before building.

Build: the choices you made yourself, and how you tested them.

Ship: how it went out, what you watched afterwards and what you'd do differently.

Sample spoken answer:

“At my last company I owned autosave for a long application form. The ticket just said 'save progress', so first I asked the product owner what happens with two open tabs and whether a half-filled invalid field should save. We agreed the latest save wins and we save drafts even if invalid. I built it so changes save a moment after the user stops typing, with a small 'Saved' or 'Offline, will retry' label. The choices I made myself were saving only the changed fields to keep requests small, and queueing saves while offline. I wrote unit tests for the queue and a browser test for the full flow, and we released it behind a feature flag to a small group first. Watching errors that week, I found a bug on slow networks where an older save finished after a newer one, and fixed it by sending a version number.”

Red flag to avoid:

Describing only the code you typed, with no questions asked, no testing and no idea what happened after release.

They may ask next:
  • What would you do differently if you built it again?
  • How did you know the feature was actually working for users after release?
Say it in 60 seconds
Medium Behavioral round Mid-level Practice question

22. What is one comment a reviewer left on your pull request that you still think about when you write JavaScript today?

What the interviewer is really testing:
Whether you take feedback well, understand the reason behind it, and turn it into a lasting habit.
Answer frame:

The feedback: what the reviewer pointed out, in your own code.

Why it mattered: the bug or cost it would have caused.

The habit: what you do differently now, and whether you passed it on.

Sample spoken answer:

“Early on I wrote a helper that took the list of orders from our state and called sort on it before returning it. A senior reviewer asked me what else was holding that array. I hadn't thought about it: sort works in place, so my helper was quietly reordering the same list another part of the page was showing, and that explained a bug we'd seen where a table jumped around. Her point was that a function shouldn't change what it's given unless that's its whole job. Since then I treat inputs as read-only. I use toSorted, spread copies, and map instead of changing things in place, and when I do mutate on purpose, I name the function so it's obvious. I now leave that same comment on other people's pull requests when I spot it.”

Red flag to avoid:

Saying you never got useful feedback, or describing a comment you accepted without understanding why.

They may ask next:
  • Tell me about feedback you disagreed with. How did you handle it?
  • What do you look for first when you review someone else's JavaScript?
Say it in 60 seconds
Medium Behavioral round Mid-level Practice question

23. Tell me about a JavaScript task you estimated badly. What did you miss, and how do you estimate now?

What the interviewer is really testing:
Whether you can be honest about a miss, name the real cause and show a better way of estimating since.
Answer frame:

The task: what you estimated and how far off it was.

What you missed: the specific unknowns, not just 'it was harder than expected'.

Now: how you break work down, flag risk and speak up early.

Sample spoken answer:

“I once said two days for adding a file upload with a preview to our profile page. It took more than a week. The upload itself was quick. What I hadn't counted was that photos from phones came in huge and sometimes rotated, so I needed resizing and orientation handling in the browser, plus a progress bar, a cancel option and proper error messages when the upload failed halfway. Then testing on real phones turned up two more issues. I also waited too long to tell my lead, hoping I'd catch up. Now I split a task into the happy path and the edge cases, estimate each one, and do a small spike first when something is new to me. And I raise it the day I see an estimate slipping, not at the deadline.”

Red flag to avoid:

Blaming the requirements or other people entirely, with nothing you would do differently.

They may ask next:
  • How do you handle being asked for an estimate on the spot in a meeting?
  • What did your lead say when you told them it would be late?
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