Retry failed API requests in React by re-running a request after an error, usually with a limit and a short delay. Decide which failures are worth retrying, then add automatic retries or a manual Retry button. This guide covers both.
Decide what is worth retrying
Retry only errors that might go away. A network failure or a 5xx response is often temporary, so a second attempt can succeed. A 4xx response means the request itself is wrong, and retrying just repeats the same rejection.
- Retry network failures and 5xx responses, plus 408 and 429 when the response marks them as retryable.
- Do not retry 400, 401, 403, or 404, because the request needs to change.
- Respect a Retry-After header when a 429 response provides one.
This decision happens in the catch path, where you either throw and stop or wait and try again. Keeping it explicit prevents a retry loop from hammering a server that is already failing. For a request that fails only occasionally, this guard is the difference between a quick recovery and an endless loop of identical failures.
Write a retry loop
A retry loop wraps the request, waits between failures, and gives up after a set number of attempts. It keeps the retry logic in one function so every caller gets the same behavior.
async function fetchWithRetry(url, { retries = 3 } = {}) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`Status ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === retries) throw error;
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
}
}
}The loop tries up to three times. After each failure except the last, it waits before the next attempt, with the delay growing from one second to two. You can swap that linear delay for an exponential backoff, but a small increasing delay is enough to start.
The catch block decides whether to keep going. On the final attempt it re-throws, so the caller still sees the real error instead of an undefined result. Between attempts, the promise returned by setTimeout pauses the loop without blocking the rest of the page.
Use the loop from a component
Call the retry helper from the same Effect you would use for a plain fetch, and keep the ignore flag so a stale response cannot win. The helper hides the retry details, so the Effect reads almost the same as a single fetch.
useEffect(() => {
let ignore = false;
fetchWithRetry("/api/posts")
.then((data) => {
if (!ignore) setPosts(data);
})
.catch(() => { if (!ignore) setStatus("error"); });
return () => { ignore = true; };
}, []);The helper resolves with parsed JSON on success and rejects after the final failed attempt. The component only sees the end result, so the loading state stays true through the whole retry sequence and the user sees one continuous wait rather than a flicker between attempts.
When the helper finally rejects, the catch sets the error state for the view to display. The underlying request setup is the same one explained in how to fetch API data in React.
Let the user retry manually
A manual Retry button is often safer than automatic retries, because it does not multiply load on a struggling server. It also gives users a way to recover without waiting on an automatic schedule. Drive the Effect with an attempt counter, and render the button only in the error state.
const [attempt, setAttempt] = useState(0);
useEffect(() => {
let ignore = false;
fetch("/api/posts")
.then((res) => res.json())
.then((data) => {
if (!ignore) setPosts(data);
})
.catch(() => { if (!ignore) setStatus("error"); });
return () => { ignore = true; };
}, [attempt]);Bumping the counter changes the dependency, so React cleans up the previous Effect and runs it again, which restarts the request from the loading state.
{status === "error" && (
<button type="button" onClick={() => setAttempt((n) => n + 1)}>
Retry
</button>
)}The button appears only after a failure and sends the view back to loading through a new attempt. Combining this with the status model from how to handle loading, error, empty, and success states keeps the screen consistent. When the user can also change the request, stop the old one first, as shown in how to cancel fetch requests in React with AbortController.
For background data with no user present, automatic retries make more sense, but keep the attempt cap and the delay.
Rune AI
Key Insights
- Retry network and 5xx errors, not 4xx errors.
- Cap the number of attempts.
- Add a delay between attempts.
- Guard against stale responses with an ignore flag.
- Prefer a manual Retry button while the behavior is still settling.
Frequently Asked Questions
Which errors should I retry?
Should retries be automatic or manual?
How do I avoid overlapping retries?
Conclusion
Retry only errors that are likely temporary, cap the number of attempts, and add a delay between them. A manual Retry button is often safer than automatic retries, especially while you learn which failures recover.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.