Prevent Race Conditions with Axios Requests

When web applications trigger rapid, asynchronous API calls—such as during search-as-you-type inputs, tab switching, or rapid filter toggling—network latency can cause responses to arrive out of order. This results in UI race conditions where older data overwrites the latest user action. This guide details how to resolve UI state race conditions using Axios through request cancellation, response validation tokens, and request debouncing.

1. Cancel Stale Requests Using AbortController

The most effective way to prevent UI race conditions is to cancel previous in-flight requests whenever a new request is triggered. Since Axios v0.22.0, this is natively supported using the standard AbortController API.

import axios from 'axios';

let abortController = null;

async function searchData(query) {
  // Cancel previous pending request if it exists
  if (abortController) {
    abortController.abort();
  }

  // Instantiate a new controller for the current request
  abortController = new AbortController();

  try {
    const response = await axios.get(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: abortController.signal
    });
    
    // Safely update UI state with latest response
    updateUI(response.data);
  } catch (error) {
    if (axios.isCancel(error)) {
      // Request was canceled; ignore and do not update UI
      return;
    }
    // Handle legitimate errors
    handleError(error);
  }
}

Canceling previous requests frees up network resources and guarantees that canceled responses will throw a cancellation error, safely bypassing your UI state updater.

2. Request Identifier and Timestamp Comparison

If canceling requests at the network layer is not feasible, you can maintain an incremental counter or timestamp in your component's state or scope. Responses are only applied to the UI if their identifier matches the most recent request.

let latestRequestId = 0;

async function fetchDetails(itemId) {
  const currentRequestId = ++latestRequestId;

  try {
    const response = await axios.get(`/api/items/${itemId}`);
    
    // Only update UI if this is still the most recent request
    if (currentRequestId === latestRequestId) {
      updateUI(response.data);
    }
  } catch (error) {
    if (currentRequestId === latestRequestId) {
      handleError(error);
    }
  }
}

This approach allows all requests to complete over the wire but guarantees that obsolete responses are ignored before they can overwrite the active UI state.

3. Implement Debouncing on User Input

Preventing excessive network calls reduces the frequency of race conditions before they occur. Applying a debounce function ensures that an Axios request is only sent after the user has stopped triggering events for a specified duration.

import debounce from 'lodash.debounce';

const debouncedFetch = debounce((query) => {
  searchData(query);
}, 300);

function onInputChange(event) {
  debouncedFetch(event.target.value);
}

Combining debouncing with AbortController ensures optimal performance by minimizing request volume while strictly enforcing sequential UI updates.