Retrying a failed async/promise function?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Retrying a failed asynchronous operation in JavaScript is a common need, but there is one critical rule: retry the function that creates the promise, not the already-settled promise object itself. A promise represents one attempt. Once it has resolved or rejected, it cannot be restarted. Good retry logic therefore wraps a function and decides when to call it again.
Retry the Function, Not the Promise
This is the wrong mental model:
That promise is already tied to a single request attempt.
This is the right model:
Now each call to makeRequest() creates a fresh promise and a fresh network attempt.
That distinction is the foundation of every retry helper.
A Simple Retry Helper
Here is a basic async retry wrapper:
Usage:
This retries up to three attempts with a one-second delay between failures.
Add Exponential Backoff
Constant delays are fine for simple cases, but exponential backoff is often better for transient failures such as rate limiting or temporary network issues.
This reduces the pressure on the failing service compared with hammering it immediately again.
Retry Only When It Makes Sense
Not every failure should be retried. Retrying makes sense for:
- temporary network issues
- '
503 Service Unavailable' - rate limiting with backoff
- timeouts
Retries are often a bad idea for:
- validation failures
- authentication errors
- clearly malformed requests
- non-idempotent actions unless you are certain repeated attempts are safe
The retry wrapper can incorporate that logic:
That prevents pointless retries on errors that will not fix themselves.
Idempotency Matters
Be careful with operations that create side effects. Retrying:
- '
GET' - some safe reads
is usually straightforward.
Retrying:
- payments
- record creation
- external side-effect APIs
can be dangerous unless the operation is idempotent or protected by an idempotency key.
This is one of the most important production concerns around retry logic. A technically correct retry helper can still cause business-level bugs if it blindly replays unsafe operations.
Abort and Cancellation
In UI code, retries should usually be cancellable. If the user navigates away, continuing to retry in the background may be wasteful or incorrect.
With fetch, this often means using an AbortController and deciding whether cancellation should stop the retry loop entirely.
That is not always necessary in a tiny utility, but it matters in real applications and background data loaders.
Common Pitfalls
The biggest mistake is retrying the same promise object instead of calling a function that creates a new promise each time. Settled promises do not restart.
Another issue is retrying every error indiscriminately. Some failures are permanent until the request or credentials change.
Developers also often forget about idempotency. Retrying a side-effecting action can duplicate work or create inconsistent state.
Finally, constant tight retries can make a failing service worse. Backoff and retry limits are part of the solution, not optional extras.
Summary
- Retry the async function that creates the promise, not the settled promise object.
- A good retry helper wraps
await fn()in a loop with a maximum retry count. - Exponential backoff is usually better than immediate repeated retries.
- Only retry errors that are likely to be transient.
- Be especially careful when retrying side-effecting or non-idempotent operations.

