Skip to Content
Course content

67: Practice Exercise: Building a Retry Wrapper for Fetch Calls

Click on the "Edit" button in the top corner of the screen to edit your slide content.

We've all been there: you're calling an API, and for some reason, it fails. Maybe it's a momentary flicker in the user's Wi-Fi, or maybe the server is just having a bad Tuesday. Your first instinct is usually to just "try it again." It seems simple enough, but if you implement this blindly, you can actually make a bad situation much worse.

The danger of the immediate loop

When I first started dealing with unreliable endpoints, I used to write things that looked like this. I'd wrap my fetch in a while loop and just keep hitting the endpoint until it finally returned a 200 OK.

async function naiveFetch(url) {
  let success = false;
  let data;
  while (!success) {
    try {
      const response = await fetch(url);
      data = await response.json();
      success = true;
    } catch (e) {
      console.log("Failed, trying again immediately...");
    }
  }
  return data;
}

On the surface, it "works." But in a production environment, this is a nightmare. If the server is down because it's overloaded, you aren't helping it recover; you're hammering it with requests as fast as the JavaScript event loop can cycle. You're essentially launching a mini-DoS attack against your own infrastructure. Plus, if the error is something permanent—like a 404 Not Found—this loop will run forever, freezing your application logic and eating up memory.

Adding a cooling-off period

To do this right, we need to introduce two things: a maximum retry limit and a delay. I prefer using "exponential backoff." Instead of waiting 1 second every time, we increase the wait time after each failure. This gives the server actual breathing room to recover.

Since setTimeout doesn't return a promise, we have to wrap it in one to make it work with our async/await flow. I usually write a small helper for this because it keeps the main logic clean.

const wait = (ms) => new Promise(res => setTimeout(res, ms));

async function smartFetch(url, options = {}, retries = 3, backoff = 1000) {
  try {
    const response = await fetch(url, options);
    
    // Fetch only rejects on network errors. 
    // We need to manually check for server errors (500s).
    if (!response.ok) {
      if (response.status >= 500 && retries > 0) {
        throw new Error(`Server Error: ${response.status}`);
      }
      // If it's a 400-level error, retrying won't help. Stop immediately.
      return response; 
    }
    
    return response;
  } catch (err) {
    if (retries <= 0) throw err;
    
    console.log(`Retrying in ${backoff}ms... (${retries} attempts left)`);
    await wait(backoff);
    
    // We double the backoff for the next attempt
    return smartFetch(url, options, retries - 1, backoff * 2);
  }
}

Why the status code check matters

Notice the distinction I made between 400-level and 500-level errors. This is a detail that often gets missed. If a server tells you 404 Not Found or 401 Unauthorized, it's telling you that the request itself is the problem. No amount of retrying is going to magically make that resource appear or make your API key valid. Retrying those is just wasted CPU cycles.

However, a 503 Service Unavailable or a 504 Gateway Timeout usually means the server is momentarily overwhelmed. That's the "sweet spot" for a retry wrapper. By combining a retry limit, a growing delay, and a filter for retryable status codes, you transform a brittle network call into a resilient one.




📋 Practical Task

Exercise: Build a Resilient Weather API Wrapper

Imagine you are integrating a third-party weather service that is notoriously flaky. You need to create a robust wrapper function called fetchWeatherWithRetry.

Requirements:

  • The function should accept a url and a maxRetries number.
  • It must use a delay between attempts (start with 500ms and double it each time).
  • It should only retry if the error is a network failure (the fetch promise rejects) or if the response status is 500 or higher.
  • If the response is a 400-level error, it should return the response immediately without retrying.
  • If the maxRetries limit is reached, it should throw a final error: "Maximum retries exceeded".

Test your code by:
Mocking a fetch call that fails twice with a 503 status before finally returning a 200 OK on the third attempt.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.