Retry Transient Errors

Avatar of Hemanta SundarayHemanta Sundaray

HttpClient.retryTransient() is a purpose-built retry combinator for the most common case: retrying requests that fail due to temporary issues like network errors, timeouts, or rate limiting.

http.ts
import {
FetchHttpClient,
HttpClient,
HttpClientRequest,
} from "effect/unstable/http";
import { Effect, Schedule } from "effect";
function fetchWithTransientRetry(userId: number) {
return Effect.gen(function* () {
const client = (yield* HttpClient.HttpClient).pipe(
HttpClient.filterStatusOk,
HttpClient.retryTransient({
times: 3,
schedule: Schedule.exponential("500 millis"),
}),
);
const request = HttpClientRequest.get(
`https://dummyjson.com/users/${userId}`,
);
const response = yield* client.execute(request);
const user = yield* response.json;
return user;
}).pipe(Effect.provide(FetchHttpClient.layer));
}
// Test it
Effect.runPromise(fetchWithTransientRetry(1)).then(
(user) => console.log("User:", user.firstName, user.lastName),
(error) => console.error("Failed after retries:", error.message),
);

Output:

Terminal
User: Emily Johnson

Unlike retry, which retries on all errors, retryTransient only retries errors that are classified as transient. Specifically, it retries when:

  • The error is a TimeoutException (from Effect.timeout)
  • The error is an HttpClientError with a TransportError reason (network failures like DNS resolution, connection refused, etc.)
  • The error is an HttpClientError with a StatusCodeError reason where the status code is 408 (Request Timeout), 429 (Too Many Requests), 500 (Internal Server Error), 502 (Bad Gateway), 503 (Service Unavailable), or 504 (Gateway Timeout)

A 404 or 400 won’t be retried because those aren’t transient. Retrying them would give the same result.

You can also pass just a Schedule directly:

HttpClient.retryTransient(
Schedule.exponential("1 second").pipe(Schedule.compose(Schedule.recurs(5))),
);

The mode option lets you control what the schedule receives as input. By default, it receives both errors and responses. You can narrow this:

// Only consider errors for scheduling (ignore response status)
HttpClient.retryTransient({
mode: "errors-only",
times: 3,
schedule: Schedule.exponential("500 millis"),
});
// Only consider response status for scheduling (ignore errors)
HttpClient.retryTransient({
mode: "response-only",
times: 3,
schedule: Schedule.exponential("500 millis"),
});

If you have additional error types that should be considered transient, use the while predicate:

HttpClient.retryTransient({
times: 3,
while: (error) => error._tag === "TransportError",
});

The while predicate is checked in addition to the built-in transient checks. It lets you widen the set of retryable errors without replacing the defaults.

For most applications, retryTransient with a schedule and a times cap is the right default. It handles the common failure modes — network blips, rate limits, server hiccups, and timeouts — without retrying errors that can’t be fixed by waiting.

Sign in to save progress

Stay in the loop

Get notified when new Effect Atom related content is published.