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 chapters are added and when this course is complete.