JavaScript
asynchronous
race condition
concurrency
debugging

JavaScript asynchronous race condition

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

An asynchronous race condition happens when the final state of your JavaScript code depends on which async operation finishes last, not on which one should logically win. These bugs are common in search boxes, form autosave, and any UI that starts a new request before the previous one is finished.

A Simple Race Condition Example

Imagine a search field that fetches results every time the user types. If the request for an older query finishes after the request for a newer query, stale results can overwrite the correct ones.

javascript
1let currentResults = [];
2
3async function search(term) {
4  const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`);
5  currentResults = await response.json();
6  renderResults(currentResults);
7}
8
9search("cat");
10search("caterpillar");

If the cat request is slower than the caterpillar request, it may finish later and replace the newer results with older data.

Fix It with a Request Token

One common fix is to assign each request a version number and ignore responses that are no longer current.

javascript
1let requestId = 0;
2
3async function search(term) {
4  const id = ++requestId;
5  const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`);
6  const data = await response.json();
7
8  if (id !== requestId) {
9    return;
10  }
11
12  renderResults(data);
13}

Now only the latest request is allowed to update the UI. Older responses still finish, but they are ignored.

Cancel Old Requests with AbortController

If the environment supports it, cancelling obsolete requests is even better because it reduces wasted work.

javascript
1let controller;
2
3async function search(term) {
4  if (controller) {
5    controller.abort();
6  }
7
8  controller = new AbortController();
9
10  try {
11    const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`, {
12      signal: controller.signal
13    });
14
15    const data = await response.json();
16    renderResults(data);
17  } catch (error) {
18    if (error.name !== "AbortError") {
19      throw error;
20    }
21  }
22}

This pattern is useful for typeahead inputs and other interfaces where only the newest request matters.

Shared State Is the Real Problem

The deeper issue is not that JavaScript is multithreaded in the same way as some backend languages. The problem is that multiple asynchronous tasks are competing to update shared state such as UI data, cache entries, or a single object in memory.

Once you recognize the shared state, the fix becomes clearer. You either serialize updates, attach ownership to each request, or make outdated work unable to commit its result.

Serialize Work When Order Matters

Some operations should not overlap at all. For example, if you autosave a document and each save must happen in order, it may be better to queue writes rather than let them race. In those cases, the right fix is not cancellation but explicit sequencing.

javascript
1let saveQueue = Promise.resolve();
2
3function queueSave(payload) {
4  saveQueue = saveQueue.then(() => fetch("/api/save", {
5    method: "POST",
6    headers: { "Content-Type": "application/json" },
7    body: JSON.stringify(payload)
8  }));
9
10  return saveQueue;
11}

This pattern ensures each save starts only after the previous save has completed.

Practical Strategies

A few habits reduce race conditions significantly:

  • Keep async functions small and explicit about what state they update.
  • Prefer immutable result replacement over mutating shared objects in many places.
  • Tag requests, cancel outdated work, or queue operations when order matters.
  • Test with artificial delays so out-of-order completion happens in development, not only in production.

Common Pitfalls

  • Assuming requests will finish in the same order they were started.
  • Updating shared state from multiple async functions without ownership checks.
  • Catching every error and accidentally hiding AbortError versus real failures.
  • Fixing the UI symptom while leaving the underlying shared-state race unresolved.

Summary

  • An async race condition appears when completion order, rather than intended logic, controls the final state.
  • Search requests and autosave flows are common places where this bug appears.
  • Use request tokens or AbortController to stop stale responses from overwriting newer state.
  • Focus on the shared state being updated, because that is where the race actually happens.
  • Reproducing delayed or out-of-order responses is one of the fastest ways to debug these issues.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.