How to avoid race condition?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Hard
#JavaScript #Asynchronicity #JS Basics

Brief Answer

Race condition is a programming error that occurs when multiple asynchronous operations try to access the same data simultaneously, and the result depends on the order of their execution. To avoid race conditions, synchronization methods are used: locks, semaphores, queues, cancellation of previous operations, and proper state management.

Main prevention methods:

  • Canceling previous operations — using AbortController
  • Execution queues — sequential processing of operations
  • Locks — prohibiting simultaneous access
  • Atomic operations — indivisible operations

Full Answer

Race condition is a critical problem in asynchronous programming where program behavior depends on the timing of competing operations. This is especially relevant in web applications where multiple asynchronous operations can run in parallel.

Causes

Race conditions occur when:

  • Multiple asynchronous operations access the same data
  • Order of operation execution is unpredictable
  • No synchronization for resource access

Main Prevention Methods

1. Canceling Previous Operations

Most common approach when working with APIs:

// Cancel previous request on new search
let controller = new AbortController();
 
function search(query) {
  // Cancel previous request
  controller.abort();
  controller = new AbortController();
  
  return fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  });
}

2. Execution Queues

Process operations sequentially:

// Queue for sequential execution
const queue = Promise.resolve();
 
function addToQueue(operation) {
  return queue.then(() => operation());
}

Practical Examples

Preventing Race in Search

class SearchService {
  constructor() {
    this.controller = new AbortController();
  }
  
  async search(query) {
    // Cancel previous search
    this.controller.abort();
    this.controller = new AbortController();
    
    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: this.controller.signal
      });
      return await response.json();
    } catch (error) {
      if (error.name !== 'AbortError') {
        throw error;
      }
    }
  }
}

Synchronizing State Access

// Use mutex for synchronization
class Mutex {
  constructor() {
    this.queue = Promise.resolve();
  }
  
  lock() {
    let unlock;
    const lock = new Promise(resolve => {
      unlock = resolve;
    });
    
    this.queue = this.queue.then(() => lock);
    return unlock;
  }
}

Common Scenarios

1. Auto-saving Data

When quickly editing data:

// Correct approach - cancel previous save
function autoSave(data) {
  if (this.saveController) {
    this.saveController.abort();
  }
  
  this.saveController = new AbortController();
  return saveData(data, this.saveController.signal);
}

2. UI Updates

When multiple operations update the same element:

// Use sequence for correct order
async function updateUI() {
  const results = await Promise.all([
    fetch('/api/data1'),
    fetch('/api/data2')
  ]);
  
  // Update UI only after all data
  renderResults(results);
}

Best Practices

  1. Cancel unnecessary operations — resource savings
  2. Use queues for critical sections — predictable order
  3. Apply mutexes for shared resources — safe access
  4. Check state before updating — avoid stale data
  5. Use transactions for complex operations — atomicity

Compatibility and Limitations

Race prevention methods work in all modern browsers but require proper understanding of asynchronicity.

Key Benefits of Preventing Races

  1. Predictability — stable application behavior
  2. Reliability — no random errors
  3. Performance — avoiding unnecessary operations
  4. Improved UX — correct data display

Preventing race conditions is an important aspect of creating reliable asynchronous applications. Proper synchronization ensures stable operation and predictable behavior.


Knowledge Check Task

Task

What’s the problem with this code and how to fix it?

let userData = null;
 
async function updateProfile(newData) {
  const response = await fetch('/api/profile', {
    method: 'PUT',
    body: JSON.stringify(newData)
  });
  
  userData = await response.json();
  renderProfile(userData);
}
 
// User quickly changes data
updateProfile({ name: 'John' });
updateProfile({ name: 'Peter' });
updateProfile({ name: 'Sid' });
View answer

Answer: The problem is a race condition. Since requests execute asynchronously, the order of completion is unpredictable. It may happen that the last request sent completes first, and outdated data is displayed in the interface.

Fixed version:

let userData = null;
let controller = new AbortController();
 
async function updateProfile(newData) {
  // Cancel previous request
  controller.abort();
  controller = new AbortController();
  
  try {
    const response = await fetch('/api/profile', {
      method: 'PUT',
      body: JSON.stringify(newData),
      signal: controller.signal
    });
    
    userData = await response.json();
    renderProfile(userData);
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Profile update error:', error);
    }
  }
}

Explanation:

  1. Each new call cancels the previous request
  2. Only the last request updates data in the interface
  3. Cancelled requests don’t cause user errors
  4. User sees result of the last action

This is a standard pattern for preventing races in web applications. It ensures the interface displays current state after the last user action.


Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site and improve yourself every day 💪