-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
67: Practice Exercise: Building a Retry Wrapper for Fetch Calls
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
urland amaxRetriesnumber. - 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
fetchpromise 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
maxRetrieslimit 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.
There are no comments for now.