JavaScript Secrets: How to Implement Retry Logic Like a Pro
Tags: webdev, javascript, programming, productivity
Published: 2024-04-11 08:13:06

## Introduction
As enthusiastic JavaScript users, we often rely on REST APIs and microservices to fetch data for our applications, or to create and update information. Sometimes, we encounter issues like temporary errors, unstable connections, or service downtimes. These can disrupt our applications because we might not have effective strategies in place to try the operation again, leading to a disappointing user experience. 🌐💡

To improve this situation and ensure our applications run smoothly, it’s essential to implement solid retry strategies. This approach allows our applications to handle disruptions gracefully, maintaining stability and keeping users happy. By preparing our applications to retry failed operations intelligently, we enhance their resilience against common network issues. Let’s make our applications more reliable and user-friendly by mastering retry strategies. 🚀🛡️

[Click here for GitHub Repo for code samples](https://github.com/officialanurag/tech-articles/tree/main/rerty-strategies-js)

Let’s jump straight into exploring different methods to implement retry logic! 🤿

**So, what are retry strategies?**

Imagine your application as a determined traveler navigating through a landscape riddled with obstacles. Just as a traveler might encounter closed roads or bad weather, our applications often face similar hurdles in the digital realm — like a sudden service outage or a glitch in the network. Retry strategies are our map and compass in these scenarios, guiding our application on when and how to make another attempt to reach its destination successfully. 🗺️⏳

They help our applications understand whether a problem is just a temporary glitch or something more serious. Based on this understanding, our applications can decide to wait a bit and try again, perhaps taking a slightly different path by adjusting the timing or method of the request.

---

### Below are some strategies (in short):
Let’s explore different retry strategies through code examples having simulation of request failure as well, each offering a distinct method for overcoming digital challenges and keeping our applications on track. 🛤️

**📈 Exponential Backoff**
Think of this as gradually increasing your steps as you try to leap over a puddle. Initially, you start with small jumps, but with each attempt, you increase your leap exponentially. This strategy helps lessen the burden on our digital pathways, making it less likely for our efforts to splash down into the water again.

> Doubles the wait time after each retry to reduce system load and failure likelihood.

```javascript
const axios = require('axios');

let attemptCounter = 0; // Keep track of attempts

/**
 * Attempts to fetch data from a given URL using axios with simulated failures.
 * The function simulates network failures for the first 2 attempts by throwing an error,
 * demonstrating how retry mechanisms can handle transient errors.
 * After the simulated failures, actual axios requests are made.
 * 
 * Parameters:
 * - url: The URL to fetch data from.
 * - retries: The number of retries allowed.
 * - delay: The initial delay before retrying, which doubles with each retry.
 * 
 * The function uses exponential backoff for the delay between retries,
 * effectively handling temporary network issues by giving time for recovery.
 */
const fetchData = async (url, retries, delay) => {
  try {
    attemptCounter++;
    if (attemptCounter <= 2) {
      throw new Error('Simulated network failure');
    }

    const response = await axios.get(url);
    console.log(`Success: ${response.status}`);
    return response.data;
  } catch (error) {
    console.log(`Attempt ${attemptCounter} failed with error: ${error.message}. Waiting ${delay} ms before retrying.`);
    if (retries > 0) {
      await new Promise(resolve => setTimeout(resolve, delay));
      return fetchData(url, retries - 1, delay * 2);
    } else {
      throw new Error('All retries failed');
    }
  }
};

const url = 'https://jsonplaceholder.typicode.com/posts/1';
fetchData(url, 3, 1000).catch(console.error);

/**
 * Output:
 * Attempt 1 failed with error: Simulated network failure. Waiting 1000 ms before retrying.
 * Attempt 2 failed with error: Simulated network failure. Waiting 2000 ms before retrying.
 * Success: 200
 */
```

**➡️ ️Linear Backoff**
This is like walking on a straight path, where you take a step forward at regular intervals. After each retry, you wait a bit longer, but the wait time increases by the same amount each time. It’s steady and predictable, making sure we don’t rush and stumble.

> Increases wait time by a constant amount after each retry for predictable delays.

```javascript
const axios = require('axios');

let attemptCounter = 0; // Tracks the number of attempts for simulating failures

/**
 * This function demonstrates a linear backoff retry strategy using axios for HTTP requests.
 * It simulates network failures for the initial attempts and then succeeds, showcasing
 * how applications can recover from transient issues with appropriate retry logic.
 *
 * The linear backoff strategy increases the delay between retries by a fixed increment,
 * providing a balanced approach to managing retry intervals and allowing the system
 * some time to recover before the next attempt.
 *
 * Parameters:
 * - url: The URL to fetch data from.
 * - retries: The total number of retries allowed.
 * - delay: The initial delay before the first retry.
 * - increment: The amount by which the delay increases after each retry.
 *
 * On failure, the function waits for the specified delay, then retries the request
 * with an increased delay, based on the linear backoff calculation.
 */
const fetchDataWithLinearBackoff = async (url, retries, delay, increment) => {
  try {
    attemptCounter++;
    if (attemptCounter <= 3) {
      throw new Error('Simulated network failure');
    }

    const response = await axios.get(url);
    console.log(`Success: ${response.status}`);
    return response.data;
  } catch (error) {
    console.log(`Attempt ${attemptCounter} failed with error: ${error.message}. Waiting ${delay} ms before retrying.`);
    if (retries > 0) {
      await new Promise(resolve => setTimeout(resolve, delay));
      return fetchDataWithLinearBackoff(url, retries - 1, delay + increment, increment);
    } else {
      throw new Error('All retries failed');
    }
  }
};

const url = 'https://jsonplaceholder.typicode.com/posts/1';
fetchDataWithLinearBackoff(url, 5, 1000, 2000).catch(console.error);

/**
 * Output:
 * Attempt 1 failed with error: Simulated network failure. Waiting 1000 ms before retrying.
 * Attempt 2 failed with error: Simulated network failure. Waiting 3000 ms before retrying.
 * Attempt 3 failed with error: Simulated network failure. Waiting 5000 ms before retrying.
 * Success: 200
 */
```

**🕛 Fixed Delay**
Imagine pausing to breathe at regular intervals, no matter how far you’ve run. This strategy keeps the waiting time the same between each retry, providing our applications with a consistent rhythm to follow, ensuring they don’t wear themselves out too quickly.

> Maintains a constant wait time between retries, regardless of attempt count.

```javascript
const axios = require('axios');

let attemptCounter = 0; // To track the attempt number for simulating a scenario

/**
 * Demonstrates implementing a fixed delay retry strategy for HTTP requests using axios.
 * It simulates failures for the initial attempts to illustrate how fixed delay retries
 * can effectively manage transient errors by waiting a predetermined amount of time
 * before each retry attempt, regardless of the number of attempts made.
 *
 * The fixed delay approach ensures that retries are spaced out by a consistent interval,
 * offering a straightforward and predictable method to allow for system recovery or error
 * resolution before the next attempt. This strategy is particularly useful in scenarios
 * where the expected recovery time is consistent.
 *
 * Parameters:
 * - url: The endpoint URL to make the HTTP GET request to.
 * - retries: The number of retry attempts before giving up.
 * - delay: The fixed time in milliseconds to wait before each retry attempt.
 */
const fetchData = async (url, retries, delay) => {
  try {
    attemptCounter++;

    if (attemptCounter <= 3) {
      throw new Error('Simulated network failure');
    }

    const response = await axios.get(url);
    console.log(`Success: ${response.status}`);
    return response.data;
  } catch (error) {
    console.log(`Attempt ${attemptCounter} failed with error: ${error.message}. Waiting ${delay} ms before retrying.`);
    if (retries > 0) {
      await new Promise(resolve => setTimeout(resolve, delay));
      return fetchData(url, retries - 1, delay);
    } else {
      throw new Error('All retries failed');
    }
  }
};

const url = 'https://jsonplaceholder.typicode.com/posts/1';
fetchData(url, 4, 1000).catch(console.error);

/**
 * Output:
 * Attempt 1 failed with error: Simulated network failure. Waiting 1000 ms before retrying.
 * Attempt 2 failed with error: Simulated network failure. Waiting 1000 ms before retrying.
 * Attempt 3 failed with error: Simulated network failure. Waiting 1000 ms before retrying.
 * Success: 200
 */
```

**🌀 Fibonacci Backoff**
Inspired by nature’s spiral, this approach increases the wait time following the elegant Fibonacci sequence. Each step forward combines the wisdom of the last two, finding a harmonious balance between rushing and waiting too long, guiding us through challenges with natural grace.

> Uses the Fibonacci sequence to determine the wait time, balancing between aggressive and moderate delays.

```javascript
const axios = require('axios');

let attemptCounter = 0; // Tracks the current attempt for simulation

/**
 * Calculates the Fibonacci number for a given index.
 * The Fibonacci sequence is a series of numbers where each number is the sum
 * of the two preceding ones, usually starting with 0 and 1.
 *
 * - index: The position in the Fibonacci sequence.
 */
const calculateFibonacciNumber = (index) => {
    if (index <= 1) return index;
    let previous = 0, current = 1, temp;
    for (let i = 2; i <= index; i++) {
        temp = previous + current;
        previous = current;
        current = temp;
    }
    return current;
};

/**
 * Performs an HTTP GET request using axios with retries based on the Fibonacci backoff strategy.
 * Initially simulates network failures for the first few attempts to illustrate how the application
 * recovers using retry strategies with increasing delays based on the Fibonacci sequence.
 *
 * - url: The URL to send the request to.
 * - retries: The number of retries allowed before failing.
 * - baseDelay: The base delay in milliseconds for the Fibonacci backoff calculation.
 */
const fetchData = async (url, retries, baseDelay) => {
  try {
    attemptCounter++;

    if (attemptCounter <= 2) {
      throw new Error('Simulated network failure');
    }

    const response = await axios.get(url);
    console.log(`Success: ${response.status}`);
    return response.data;
  } catch (error) {
    console.log(`Attempt ${attemptCounter} failed with error: ${error.message}. Waiting for the next attempt.`);
    if (retries > 0) {
      const delay = calculateFibonacciNumber(5 - retries + 1) * baseDelay;
      console.log(`Waiting ${delay} ms before retrying.`);
      await new Promise(resolve => setTimeout(resolve, delay));
      return fetchData(url, retries - 1, baseDelay);
    } else {
      throw new Error('All retries failed after ' + attemptCounter + ' attempts');
    }
  }
};

const url = 'https://jsonplaceholder.typicode.com/posts/1';
fetchData(url, 5, 100).catch(console.error);

/** 
 * Output:
 * Attempt 1 failed with error: Simulated network failure. Waiting for the next attempt.
 * Waiting 100 ms before retrying.
 * Attempt 2 failed with error: Simulated network failure. Waiting for the next attempt.
 * Waiting 100 ms before retrying.
 * Success: 200
 */
```

**🎲 Randomised Retry**
As if rolling dice to decide how long to wait, this strategy introduces an element of chance in our retry timing. This randomness helps distribute our attempts more evenly, ensuring that not everyone rushes through the door at once, reducing the pressure on our systems.

> Selects a random wait time before retrying to distribute attempts and reduce system load.

```javascript
const axios = require('axios');

let attemptCounter = 0; // To track the number of attempts and simulate network failures

/**
 * Performs an HTTP GET request using axios with retries that incorporate randomized delays.
 * This strategy simulates network failures for the initial attempts to demonstrate how the application
 * can recover by retrying with random delays between a specified minimum and maximum range.
 * The randomization of retry intervals helps distribute the load and reduce peak pressure on the system.
 *
 * - url: The endpoint URL for the HTTP GET request.
 * - retries: The number of retries before giving up.
 * - minDelay: The minimum delay in milliseconds before retrying.
 * - maxDelay: The maximum delay in milliseconds before retrying.
 */
const fetchData = async (url, retries, minDelay, maxDelay) => {
  try {
    attemptCounter++;

    if (attemptCounter <= 2) {
      throw new Error('Simulated network failure');
    }

    const response = await axios.get(url);
    console.log(`Success: ${response.status}`);
    return response.data;
  } catch (error) {
    console.log(`Attempt ${attemptCounter} failed with error: ${error.message}.`);
    if (retries > 0) {
      const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay; // Calculate a random delay
      console.log(`Waiting ${Math.round(randomDelay)} ms before retrying.`);
      await new Promise(resolve => setTimeout(resolve, Math.round(randomDelay)));
      return fetchData(url, retries - 1, minDelay, maxDelay);
    } else {
      throw new Error(`All retries failed after ${attemptCounter} attempts`);
    }
  }
};

const url = 'https://jsonplaceholder.typicode.com/posts/1';
fetchData(url, 3, 500, 1500).catch(console.error);

/**
 * Output:
 * Attempt 1 failed with error: Simulated network failure.
 * Waiting 1487 ms before retrying.
 * Attempt 2 failed with error: Simulated network failure.
 * Waiting 777 ms before retrying.
 * Success: 200
 */
```

**⚡ Immediate Retry**
Sometimes, the best approach is to try again right away, especially when we sense a quick solution might be just around the corner. This strategy is all about seizing the moment, ready to spring back into action at the first sign of a hiccup.

> Retries immediately without delay, ideal for quickly resolvable issues.

```javascript
const axios = require('axios');

let attemptCounter = 0; // Tracks the current attempt number to simulate network failures

/**
 * Executes an HTTP GET request using axios, employing an immediate retry strategy upon failure.
 * This approach simulates network failures for the first few attempts, illustrating how the application
 * adapts by retrying the operation immediately without any delay, making it suitable for quickly resolvable
 * transient issues.
 *
 * Immediate retries are used in scenarios where there is a high probability that errors are temporary
 * and can be resolved without introducing a delay, effectively improving the chances of a successful request
 * in environments with fluctuating network stability.
 *
 * - url: The endpoint URL for the HTTP GET request.
 * - retries: The number of allowed retries before giving up.
 */
const fetchData = async (url, retries) => {
  try {
    attemptCounter++;

    if (attemptCounter <= 3) {
      throw new Error('Simulated network failure');
    }

    const response = await axios.get(url);
    console.log(`Success: ${response.status}`);
    return response.data;
  } catch (error) {
    console.log(`Attempt ${attemptCounter} failed with error: ${error.message}.`);
    if (retries > 0) {
      console.log('Retrying immediately.');
      return fetchData(url, retries - 1);
    } else {
      throw new Error(`All retries failed after ${attemptCounter} attempts`);
    }
  }
};

const url = 'https://jsonplaceholder.typicode.com/posts/1';
fetchData(url, 5).catch(console.error);

/**
 * Output:
 * Attempt 1 failed with error: Simulated network failure.
 * Retrying immediately.
 * Attempt 2 failed with error: Simulated network failure.
 * Retrying immediately.
 * Attempt 3 failed with error: Simulated network failure.
 * Retrying immediately.
 * Success: 200
 */
```

---

### Utilizing `npm` Packages for Retry Logic**
**axios-retry**
`axios-retry` offers a straightforward way to add retry functionality to your `axios` requests. This library can automatically retry failed requests under certain conditions, such as network errors or receiving specific HTTP response codes, and it supports configurable retry strategies, including exponential backoff.

To use `axios-retry` in a your application, first, ensure you have `axios` and `axios-retry` installed.

```javascript
npm install axios axios-retry
```
or

```javascript
yarn add axios axios-retry
```
Then, you can configure `axios-retry` in your application like so:

```javascript
import axios from 'axios';
import axiosRetry from 'axios-retry';

// Configure axios-retry to automatically retry requests
axiosRetry(axios, {
  retries: 3, // Number of retry attempts
  retryDelay: axiosRetry.exponentialDelay, // Use exponential backoff delay between retry attempts
});

// Example of making a request with axios that will be retried upon failure
axios.get('https://jsonplaceholder.typicode.com/posts/1')
  .then(response => console.log(response.data))
  .catch(error => console.error(error));
```
Using `axios-retry` in frontend can significantly simplify handling retries for HTTP requests in your applications, allowing you to make your web apps more robust and reliable with minimal additional code.
[npm package -> axios-retry](https://www.npmjs.com/package/axios-retry)

**retry**
The `retry` package provides a flexible way to add retry functionality to your code, suitable for any asynchronous operation or logic you want to attempt multiple times upon failure. Here's a basic guide on how to use it:

First, you need to install the package using npm or yarn:

```javascript
npm install retry
```
or

```javascript
yarn add retry
```

Here’s a simple example of how to use retry to perform a task that might fail:

```javascript
const retry = require('retry');
const axios = require('axios'); // Assuming axios is used for HTTP requests

async function fetchData(url) {
  const operation = retry.operation({
    retries: 3, // Maximum amount of retries
    factor: 2, // The exponential factor for delay
    minTimeout: 1000, // The number of milliseconds before starting the first retry
    maxTimeout: 2000, // The maximum number of milliseconds between two retries
  });

  operation.attempt(async currentAttempt => {
    try {
      const response = await axios.get(url);
      console.log('Data:', response.data);
    } catch (error) {
      console.log(`Attempt ${currentAttempt} failed: ${error.message}`);
      if (operation.retry(error)) {
        console.log(`Retrying...`);
        return;
      }
      console.error('Request failed after retries:', error.message);
    }
  });
}

fetchData('https://jsonplaceholder.typicode.com/posts/1');
```
The `retry` package is powerful and flexible, making it suitable for a wide range of retry scenarios beyond just HTTP requests. You can adapt the retry logic to fit various asynchronous tasks, such as database operations, filesystem access, or any other operations that might require retries upon failure.

[npm package -> retry](https://www.npmjs.com/package/retry)

---
**Best Practices for Implementing Retry Logic**

- Determine When to Retry: Not all errors should trigger a retry. Evaluate whether the error is transient and likely to be resolved with subsequent attempts.

- Limit Retries: Set a maximum number of retries to prevent infinite loops.

- Consider the User Experience: Ensure that retry logic does not degrade the user experience, especially in client-facing applications.

---

**Conclusion 🚀**
Incorporating retry logic into your JavaScript applications is a critical step toward ensuring reliability and resilience. Whether you choose to implement these strategies directly or leverage existing npm packages, the ability to gracefully handle transient errors will significantly enhance your application’s robustness and user satisfaction.

**Connect with me on LinkedIn:** https://www.linkedin.com/in/anu95/

**Follow me on Instagram:** 
https://www.instagram.com/code_with_onu/

Happy coding! 💻 ❤️

> Anurag Gupta
> SDE-II, Microsoft