The problem
Any agent loop that calls an API, a tool, or even just reads a file over a flaky network connection will eventually hit a call that fails for no real reason — a dropped connection, a momentary rate limit, a slow server. Without a retry, that one blip ends the whole run; with a naive immediate retry, you can hammer an already-struggling service and make things worse.
Expected outcome
By the end of this guide you will have a small wrapper that retries any failing async call with increasing delay between attempts (exponential backoff), and gives up with a clear error only after a fixed number of real attempts.
Requirements and costs
- Node.js (version 18 or newer).
- Cost: free. No dependencies.
Unfamiliar terms, explained
- Transient failure — a failure that’s likely to succeed if you just try again shortly after, as opposed to a real, permanent error (wrong credentials, a malformed request) that retrying won’t fix.
- Exponential backoff — waiting longer between each retry (e.g. 200ms, then 400ms, then 800ms) instead of a fixed gap, so repeated retries don’t pile pressure on a struggling service.
Step-by-step implementation
- Download the starter from the link below.
- Wrap any async call you want retried:
const { withRetry } = require("./retry"); const result = await withRetry(() => callSomethingFlaky(), { maxAttempts: 4 }); - Adjust
maxAttemptsandbaseDelayMsto match how patient you can afford to be — a user-facing agent might use 2-3 short attempts, a background job can afford more and longer waits. - Optionally pass
onRetryto log each retry attempt, so you can see in your own logs when something needed a second try.
A real example
// A call that fails twice, then succeeds on the third attempt
{
"result": "ok on attempt 3",
"calls": 3,
"retries": [
{ "attempt": 1, "delay": 10, "error": "simulated failure #1" },
{ "attempt": 2, "delay": 20, "error": "simulated failure #2" }
]
}
// A call that always fails, with maxAttempts: 3
{
"threw": true,
"calls": 3,
"message": "Failed after 3 attempts: always fails"
}
Notice the delay doubling between retries (10, then 20 in this example, using a short base delay for the test) — that’s the backoff working as intended, not a bug.
Verification and troubleshooting
What we tested, on 5 October 2026 (record TNT-TEST-2026-10-05-006): a function that fails on its first two calls and succeeds on the third — withRetry correctly returned the successful result after exactly 3 calls, with the recorded delays doubling each time (10ms, 20ms). A function that always fails, with maxAttempts: 3 — withRetry correctly made exactly 3 attempts, then threw a clear error naming the attempt count, rather than retrying forever or failing silently.
Troubleshooting:
- It retries errors that will never succeed (like invalid credentials).
withRetryretries every error by default — wrap your function so it only re-throws errors worth retrying, and returns/throws differently for permanent ones, if you need to distinguish them. - Your agent feels slow when something fails. That’s the backoff delay by design; lower
baseDelayMsormaxAttemptsfor latency-sensitive, user-facing calls. - You need to cancel a retry loop early (e.g. the user cancelled the request). This simple version doesn’t support cancellation — check
maxAttemptsis low enough that the worst case is acceptable, or add your own cancellation check inside the wrapped function.
Limitations and alternatives
This retries every error identically; it doesn’t distinguish “try again, this will probably work” from “this will never work, stop immediately” (like a 401 Unauthorized). For calls where that distinction matters, check the error type inside your wrapped function and re-throw a special value to skip retrying, or extend withRetry to accept a “should retry this error” check.
Free resources
The download contains the retry wrapper and a runnable demo showing both the success-after-retry and the give-up cases.
Download the retry/backoff starter (ZIP, 1 KB)
Is there a paid version?
Not yet. This is the complete method. TNT Review has sold nothing to date and has no checkout, so any price we discuss in future should be read as a hypothesis, not an offer.