Repetition & Retry
A Schedule is inert on its own — it only describes a
recurrence policy. To put one to work you hand it to an operator that re-runs an
effect according to that policy. The two core operators mirror each other:
Effect.retryre-runs an effect when it fails. The schedule’sInputis the error, so you can inspect the failure to decide whether to keep going.Effect.repeatre-runs an effect when it succeeds. The schedule’sInputis the success value, so you can repeat until some condition is met.
In both cases the effect runs once first, then the schedule decides whether to go again.
import { Effect, Random, Schema, Schedule } from "effect"
class HttpError extends Schema.TaggedErrorClass<HttpError>()("HttpError", { message: Schema.String, status: Schema.Number, retryable: Schema.Boolean}) {}
// A realistic request that sometimes returns a retryable 5xx and sometimes a// fatal 4xx.const fetchUser = Effect.fn("fetchUser")(function* (userId: string) { const roll = yield* Random.next const status = roll > 0.7 ? 200 : roll > 0.3 ? 503 : 401 if (status !== 200) { return yield* new HttpError({ message: `request for ${userId} failed`, status, retryable: status >= 500 }) } return { id: userId, name: "Ada Lovelace" } as const})
// Capped exponential backoff with jitter, but only retry retryable failures and// cap the number of attempts.const policy = Schedule.exponential("250 millis").pipe( Schedule.either(Schedule.spaced("10 seconds")), Schedule.jittered, Schedule.both(Schedule.recurs(6)), Schedule.setInputType<HttpError>(), Schedule.while(({ input }) => input.retryable))
const loadUser = fetchUser("user-123").pipe( Effect.retry(policy), // If every attempt is exhausted, escalate the typed error to a defect. Effect.orDie)Because policy declares its Input as HttpError, the while predicate can
read input.retryable. A 401 is non-retryable, so the policy stops
immediately even though attempts remain — fatal errors fail fast, transient ones
back off and retry.
Repeating successful effects
Section titled “Repeating successful effects”Effect.repeat keeps re-running an effect while it succeeds, stopping the moment
it fails or the schedule completes. This is the building block for polling and
periodic jobs.
import { Effect, Schedule } from "effect"
// Poll a health endpoint every 5 seconds, forever, until it fails.const poll = Effect.gen(function* () { const healthy = yield* checkHealth yield* Effect.log(`health: ${healthy ? "ok" : "degraded"}`)}).pipe(Effect.repeat(Schedule.spaced("5 seconds")))
declare const checkHealth: Effect.Effect<boolean>The schedule’s Output becomes the result of the whole expression. With
Schedule.recurs(n), repeat returns the number of recurrences; with a
Duration-producing schedule it returns the last delay. If you don’t need a
custom schedule input, Effect.schedule(effect, policy) is a thin alias for
Effect.repeat that always seeds the schedule with undefined.
Inline options for the common cases
Section titled “Inline options for the common cases”You don’t always need a full Schedule. Both retry and repeat accept an
options object covering the most common needs: a schedule, a times cap, and
while / until predicates.
import { Effect } from "effect"
declare const request: Effect.Effect<string, RequestError>class RequestError { readonly _tag = "RequestError" constructor(readonly retryable: boolean) {}}
// Retry at most 3 times, but only while the error says it is retryable.const withOptions = request.pipe( Effect.retry({ times: 3, while: (error) => error.retryable }))while keeps going as long as the predicate is true; until is its inverse,
stopping as soon as the predicate becomes true. Both accept a boolean or an
Effect<boolean>. For repeat, the predicate receives the success value instead
of the error.
Falling back when the policy is exhausted
Section titled “Falling back when the policy is exhausted”retryOrElse and repeatOrElse let you recover instead of propagating the final
failure. The fallback receives the last error and the schedule’s output (its
recurrence count), so you can log, serve a cached value, or degrade gracefully.
import { Effect, Schedule } from "effect"
declare const networkRequest: Effect.Effect<string, NetworkError>class NetworkError { readonly _tag = "NetworkError"}
const withFallback = networkRequest.pipe( Effect.retryOrElse( Schedule.recurs(2), (error, attempts) => Effect.gen(function* () { yield* Effect.logWarning(`giving up after ${attempts} retries`) return "cached-data" // graceful fallback value }) ))Testing scheduled effects
Section titled “Testing scheduled effects”Schedules realize their delays through the Clock, so a
test can advance simulated time with TestClock.adjust instead of waiting in
real time. A retry policy spanning minutes of backoff verifies in microseconds.
See Clock for the full pattern.