Race conditions in async code

Race Conditions in Asynchronous JavaScript: A Detailed Explanation and Solutions

Step 1: Understanding Race Conditions

What is a Race Condition?

A race condition occurs in asynchronous programming when multiple tasks execute out of order or unpredictably, leading to unexpected results or bugs.

In JavaScript, race conditions often happen when:

  • Multiple asynchronous operations depend on shared data.
  • One task completes before another in an uncontrolled manner.
  • A function tries to use data before it’s ready.

Example of a Race Condition

let user = {};

async function fetchUserData() {
  let response = await fetch("https://api.example.com/user");
  user.data = await response.json();
}

async function updateUserProfile() {
  user.data.name = "Updated Name"; // ❌ Might execute before fetchUserData completes
}

fetchUserData();
updateUserProfile(); // Potential race condition!

πŸ’₯ Issue: updateUserProfile() runs before fetchUserData() finishes, leading to user.data being undefined or incomplete.


Step 2: How JavaScript Handles Async Code

JavaScript is single-threaded, meaning it executes one task at a time. However, asynchronous functions (e.g., fetch, setTimeout, promises) execute concurrently, leading to race conditions when tasks depend on the same data.

Example of async behavior:

console.log("Start");

setTimeout(() => console.log("Timeout finished"), 1000);

console.log("End");

// Output:
// Start
// End
// Timeout finished (after 1 second)

πŸš€ The last console.log("Timeout finished") executes later, showing JavaScript’s async behavior.


Step 3: Common Examples of Race Conditions

Let’s explore different real-world cases.

1. Fetching Data While Another Function Modifies It

let userData = {};

async function fetchData() {
  let response = await fetch("https://api.example.com/user");
  userData = await response.json();
}

function modifyUserData() {
  userData.name = "Alice"; // ❌ Might execute before fetchData finishes
}

fetchData();
modifyUserData();

πŸ’₯ Problem: modifyUserData() runs before fetchData() completes, leading to incorrect data.

βœ… Solution: Use await to Ensure Order

async function main() {
  await fetchData();
  modifyUserData();
}

main(); // βœ… Ensures fetchData completes before modifying data

2. Multiple API Calls Completing in Unexpected Order

async function fetchPost() {
  let response = await fetch("https://api.example.com/post");
  let data = await response.json();
  console.log("Post:", data);
}

async function fetchComments() {
  let response = await fetch("https://api.example.com/comments");
  let data = await response.json();
  console.log("Comments:", data);
}

fetchPost();
fetchComments(); // ❌ Might complete before fetchPost

πŸ’₯ Problem: fetchComments() might complete before fetchPost(), leading to unexpected console output order.

βœ… Solution: Use await to Enforce Order

async function fetchAll() {
  await fetchPost();
  await fetchComments();
}

fetchAll(); // βœ… fetchPost runs before fetchComments

βœ… Better Solution: Use Promise.all() for Parallel Execution

async function fetchAllParallel() {
  let [post, comments] = await Promise.all([fetchPost(), fetchComments()]);
}
fetchAllParallel(); // βœ… Fetches both without race conditions

3. Race Conditions in User Input Handling

let searchResults = [];

async function fetchSearchResults(query) {
  let response = await fetch(`https://api.example.com/search?q=${query}`);
  searchResults = await response.json();
}

document.getElementById("search").addEventListener("input", (event) => {
  fetchSearchResults(event.target.value);
});

πŸ’₯ Problem: If the user types quickly ("a" -> "ab" -> "abc"), requests may return out of order, showing wrong results.

βœ… Solution: Use an AbortController to Cancel Previous Requests

let controller;

async function fetchSearchResults(query) {
  if (controller) controller.abort(); // ❌ Cancel the previous request
  controller = new AbortController();

  let response = await fetch(`https://api.example.com/search?q=${query}`, {
    signal: controller.signal,
  });

  searchResults = await response.json();
}

🎯 Now, only the latest request is processed!


Step 4: How to Fix and Avoid Race Conditions

Here are best practices to avoid race conditions in JavaScript.

βœ… 1. Use await to Ensure Sequential Execution

If operations depend on each other, use await inside an async function.

async function main() {
  await step1();
  await step2();
}

βœ… 2. Use Promise.all() for Parallel Execution

If operations don’t depend on each other, use Promise.all() to speed things up.

let [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);

βœ… 3. Use Mutex (Locking Mechanism) for Shared Data

Use a flag to prevent multiple functions from modifying data at the same time.

let isFetching = false;

async function fetchData() {
  if (isFetching) return; // Prevent duplicate calls
  isFetching = true;

  let response = await fetch("https://api.example.com/data");
  let data = await response.json();
  
  isFetching = false;
  return data;
}

βœ… 4. Use an AbortController to Cancel Unnecessary API Calls

For handling multiple network requests (e.g., search inputs).

let controller;

async function fetchData(query) {
  if (controller) controller.abort();
  controller = new AbortController();

  let response = await fetch(`https://api.example.com/search?q=${query}`, {
    signal: controller.signal,
  });

  return await response.json();
}

βœ… 5. Use Queues for Tasks that Depend on Order

If functions must execute in order, store them in a queue.

let queue = [];

async function addToQueue(task) {
  queue.push(task);
  if (queue.length === 1) {
    while (queue.length) {
      await queue[0](); // Execute first task in the queue
      queue.shift(); // Remove completed task
    }
  }
}

Step 5: Summary

🚨 Race Condition Problems

  • Multiple async tasks modify the same data
  • Uncontrolled execution order of functions
  • User inputs trigger multiple API calls out of order
  • Parallel API calls completing at different times

βœ… Solutions

  1. Use await inside async functions to enforce order.
  2. Use Promise.all() to run independent async functions in parallel.
  3. Use a flag or lock (Mutex) to avoid modifying the same data simultaneously.
  4. Use an AbortController to cancel outdated API requests.
  5. Use queues or controlled execution mechanisms to ensure ordered processing.

Final Thoughts

Race conditions are tricky because they don’t always happenβ€”they depend on timing. They cause subtle bugs that are hard to reproduce. Using the right async techniques ensures smooth execution and bug-free applications.

Would you like example projects demonstrating these solutions?

Leave a Reply

Your email address will not be published. Required fields are marked *