How to Structure JavaScript Code to Retry Async Operations After Failure?

Content verified by Anycode AI
July 21, 2024

Handling async operations in JavaScript can be a bit tricky, especially when things go sideways. Building a system to retry these operations can really beef up your app's reliability. Let’s walk through a practical way to do this.

Create a Retry Function

Start by making a retry function that handles retrying stuff when it goes wrong. This func will take in an async operation, how many retries we want, and maybe a delay between them retries.

const retry = async (fn, retries = 3, delay = 1000) => {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === retries) {
        throw new Error(`Operation failed after ${retries} attempts: ${error.message}`);
      }
      console.log(`Attempt ${attempt} failed. Retrying in ${delay} ms...`);
      await new Promise(res => setTimeout(res, delay));
    }
  }
};

Create the Async Operation

Now, let’s define the async task we want to retry. This could be anything that returns a promise, like fetching data from a server or querying a database.

const fetchData = async () => {
  const response = await fetch('https://api.example.com/data');
  
  if (!response.ok) {
    throw new Error('Network response was not ok: ' + response.statusText);
  }
  
  return await response.json();
};

Utilize the Retry Function

Use the retry function to execute your async operation. If it flops, it retries based on our earlier setup.

(async () => {
  try {
    const data = await retry(fetchData, 5, 2000); // 5 retries, 2000 ms delay
    console.log('Data fetched successfully:', data);
  } catch (error) {
    console.error('Failed to fetch data:', error);
  }
})();

Enhancements and Best Practices

Exponential Backoff

One way to upgrade this method is by using exponential backoff for delays. This means the wait time grows with each retry. It can help lessen repetitive failures.

const retryWithExponentialBackoff = async (fn, retries = 3, baseDelay = 1000) => {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === retries) {
        throw new Error(`Operation failed after ${retries} attempts: ${error.message}`);
      }
      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.log(`Attempt ${attempt} failed. Retrying in ${delay} ms...`);
      await new Promise(res => setTimeout(res, delay));
    }
  }
};

// Use it
(async () => {
  try {
    const data = await retryWithExponentialBackoff(fetchData, 5, 1000); // 5 retries, starting with 1000 ms delay
    console.log('Data fetched successfully:', data);
  } catch (error) {
    console.error('Failed to fetch data:', error);
  }
})();

Abort Signal

Another nice touch is to use an AbortController for canceling the operation if needed.

const fetchDataWithAbort = async (abortSignal) => {
  const response = await fetch('https://api.example.com/data', { signal: abortSignal });
  
  if (!response.ok) {
    throw new Error('Network response was not ok: ' + response.statusText);
  }
  
  return await response.json();
};

const abortableRetry = async (fn, retries = 3, delay = 1000) => {
  const controller = new AbortController();

  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn(controller.signal);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.error('Operation was aborted');
        return;
      }
      if (attempt === retries) {
        throw new Error(`Operation failed after ${retries} attempts: ${error.message}`);
      }
      console.log(`Attempt ${attempt} failed. Retrying in ${delay} ms...`);
      await new Promise(res => setTimeout(res, delay));
    }
  }
};

// Try aborting after a set time
setTimeout(() => {
  controller.abort();
}, 5000); // Abort after 5 seconds

// Use abortable retry
(async () => {
  try {
    const data = await abortableRetry(fetchDataWithAbort, 5, 2000); // 5 retries, 2000 ms delay
    console.log('Data fetched successfully:', data);
  } catch (error) {
    console.error('Failed to fetch data:', error);
  }
})();

Log and Monitor

Logging each attempt and failure is super useful for monitoring. It helps identify where things go wrong and how often retries occur.

const retryWithLogging = async (fn, retries = 3, delay = 1000) => {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      console.error(`Attempt ${attempt} failed: ${error.message}`);
      
      if (attempt === retries) {
        throw new Error(`Operation failed after ${retries} attempts: ${error.message}`);
      }
  
      console.log(`Retrying in ${delay} ms...`);
      await new Promise(res => setTimeout(res, delay));
    }
  }
};

// Use it
(async () => {
  try {
    const data = await retryWithLogging(fetchData, 5, 2000); // 5 retries, 2000 ms delay
    console.log('Data fetched successfully:', data);
  } catch (error) {
    console.error('Failed to fetch data:', error);
  }
})();
Have any questions?
Our CEO and CTO are happy to
answer them personally.
Get Beta Access
Anubis Watal
CTO at Anycode
Alex Hudym
CEO at Anycode