Hone logo
Hone
Problems

Robust API Calls: Implementing Retry with Exponential Backoff in JavaScript

Many real-world applications interact with external services or APIs. These services can occasionally become unavailable or experience temporary slowness, leading to failed requests. To improve the resilience of your application, implementing a retry mechanism with exponential backoff is crucial. This challenge asks you to build a function that gracefully handles transient errors by retrying an operation with increasing delays.

Problem Description

Your task is to implement a JavaScript function, retryWithExponentialBackoff, that takes an asynchronous function (operation) as input and attempts to execute it. If the operation fails (i.e., it throws an error or returns a rejected Promise), retryWithExponentialBackoff should automatically retry the operation after a delay. The delay should increase exponentially with each subsequent retry.

Key Requirements:

  1. Asynchronous Operation: The function must accept an asynchronous function (or a function that returns a Promise) as the operation to be retried.
  2. Retry Logic: If the operation fails, retry it.
  3. Exponential Backoff: The delay between retries should follow an exponential pattern. A common formula is baseDelay * (2 ** (attempt - 1)).
  4. Maximum Retries: The function should have a configurable maximum number of retry attempts.
  5. Success: If the operation succeeds at any point, the retryWithExponentialBackoff function should resolve with the successful result of the operation.
  6. Failure: If the operation fails after exhausting all retry attempts, the retryWithExponentialBackoff function should reject with the error from the final attempt.
  7. Configurability: The function should allow configuration of:
    • baseDelay: The initial delay in milliseconds.
    • maxRetries: The maximum number of times to retry.
    • (Optional) jitter: A small random variation to add to the delay to prevent thundering herd problems.

Expected Behavior:

  • The operation is called.
  • If it resolves, the promise returned by retryWithExponentialBackoff resolves with that value.
  • If it rejects, a delay is introduced.
  • After the delay, the operation is called again.
  • This process repeats up to maxRetries.
  • If the operation eventually resolves, retryWithExponentialBackoff resolves.
  • If all retries are exhausted and the operation still fails, retryWithExponentialBackoff rejects with the last encountered error.

Edge Cases:

  • The operation succeeds on the very first attempt.
  • The operation fails on the very first attempt but succeeds on subsequent retries.
  • The operation consistently fails up to maxRetries.
  • maxRetries is 0 (no retries should occur).

Examples

Example 1: Successful Operation After One Retry

// Helper function to simulate an async operation that fails once
async function flakyOperation() {
  let attempts = 0;
  return async () => {
    attempts++;
    console.log(`Attempt ${attempts}`);
    if (attempts === 1) {
      throw new Error("Temporary service error");
    }
    return "Operation successful!";
  };
}

async function runExample1() {
  const operation = await flakyOperation();
  const result = await retryWithExponentialBackoff(operation, {
    baseDelay: 100, // 100ms initial delay
    maxRetries: 3
  });
  console.log("Final Result:", result);
}

// Expected Output (timing will vary):
// Attempt 1
// Attempt 2
// Final Result: Operation successful!

Example 2: Operation Fails After Max Retries

// Helper function to simulate an operation that always fails
async function alwaysFailingOperation() {
  return async () => {
    console.log("Attempting operation...");
    throw new Error("Persistent service failure");
  };
}

async function runExample2() {
  const operation = await alwaysFailingOperation();
  try {
    await retryWithExponentialBackoff(operation, {
      baseDelay: 50, // 50ms initial delay
      maxRetries: 2
    });
  } catch (error) {
    console.error("Operation failed after all retries:", error.message);
  }
}

// Expected Output (timing will vary):
// Attempting operation...
// Attempting operation...
// Attempting operation...
// Operation failed after all retries: Persistent service failure

Example 3: Operation Succeeds on First Try

async function immediateSuccessOperation() {
  return async () => {
    console.log("Attempt 1 (immediate success)");
    return "Fast and successful!";
  };
}

async function runExample3() {
  const operation = await immediateSuccessOperation();
  const result = await retryWithExponentialBackoff(operation, {
    baseDelay: 200,
    maxRetries: 5
  });
  console.log("Final Result:", result);
}

// Expected Output:
// Attempt 1 (immediate success)
// Final Result: Fast and successful!

Constraints

  • baseDelay will be a non-negative integer representing milliseconds.
  • maxRetries will be a non-negative integer.
  • The operation will always be a function that returns a Promise or is an async function.
  • The total execution time, including retries, should be reasonably efficient for typical network operations. The focus is on correctness of the retry logic.

Notes

  • Consider using setTimeout or setInterval for implementing the delays. Remember to clear any intervals if the operation succeeds or fails early.
  • When implementing exponential backoff, be mindful of potential integer overflow for very large maxRetries values (though unlikely for typical use cases).
  • The jitter configuration is a good practice for distributed systems to avoid multiple clients retrying at precisely the same intervals, overwhelming the service. You can implement it by adding a random number between 0 and some value (e.g., baseDelay / 2) to the calculated delay.
  • The retryWithExponentialBackoff function itself should return a Promise.
Loading editor...
javascript