Encodera

Thanks for reading!

Donate to the site — we work hard for you.

How to retry a failed operation

Short answer

Call the function in a loop. If it succeeds, return the result; if it fails, try again, and after the last attempt pass the error on:

async function retry(task, attempts = 3) {
  for (let i = 1; i <= attempts; i++) {
    try {
      return await task();
    } catch (error) {
      console.log(`attempt ${i} failed: ${error.message}`);
      if (i === attempts) throw error;
    }
  }
}
 
let calls = 0;
const unstable = async () => {
  calls++;
  if (calls < 3) throw new Error('server busy');
  return 'data';
};
 
console.log(await retry(unstable));

Output:

attempt 1 failed: server busy
attempt 2 failed: server busy
data

A growing pause

Do not retry immediately: wait longer after each failure. This is called exponential backoff, and it gives the server time to recover:

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 
async function retry(task, attempts = 4, delay = 100) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await task();
    } catch (error) {
      if (i === attempts - 1) throw error;
      const wait = delay * 2 ** i;
      console.log(`waiting ${wait} ms`);
      await sleep(wait);
    }
  }
}
 
let calls = 0;
const result = await retry(async () => {
  if (++calls < 4) throw new Error('timeout');
  return 'ok';
});
 
console.log(result);

Output:

waiting 100 ms
waiting 200 ms
waiting 400 ms
ok

With fetch

Retry only temporary failures: network errors and statuses 500 and above. A 404 will not get better after a pause:

const response = await retry(async () => {
  const res = await fetch('/api/orders');
  if (res.status >= 500) throw new Error(`Server error ${res.status}`);
  return res;
});

Common mistake

Without the await in return await task(), the error of the promise is not caught inside try, and the retry never happens:

async function retryBroken(task) {
  for (let i = 1; i <= 3; i++) {
    try {
      return task();
    } catch {
      console.log('retrying');
    }
  }
}
 
try {
  await retryBroken(async () => {
    throw new Error('failed');
  });
} catch (error) {
  console.log('no retries:', error.message);
}

Output:

no retries: failed

Functions used in this recipe

  • Promise — A promise of a result that will be ready later: success or failure.
  • async function — A function that always returns a promise and lets you use await inside.
  • try...catch — Catches errors so the program does not stop.

Thanks for reading!

Donate to the site — we work hard for you.