Fallback & Retry
Recovering from an error doesn’t always mean producing a value — often you want to try again, fall back to an alternative, or give up after a deadline. Effect ships these resilience patterns as composable combinators built on top of Schedule.
import { Effect, Schedule, Schema } from "effect"
class FlakyError extends Schema.TaggedErrorClass<FlakyError>()("FlakyError", { attempt: Schema.Number}) {}
declare const callFlakyApi: Effect.Effect<string, FlakyError>
const resilient = callFlakyApi.pipe( // Retry up to 3 times with exponential backoff starting at 100ms. Effect.retry({ schedule: Schedule.exponential("100 millis"), times: 3 }), // If every retry is exhausted, fall back to a cached value. Effect.orElseSucceed(() => "cached response"), // Bound the whole thing: if it isn't done in 5 seconds, run a fallback. Effect.timeoutOrElse({ duration: "5 seconds", orElse: () => Effect.succeed("timed out, using default") }))retry — try again on failure
Section titled “retry — try again on failure”Effect.retry re-runs an effect when it fails. The simplest form just bounds the
number of attempts; pass a Schedule to control the timing.
import { Effect, Schedule } from "effect"
declare const request: Effect.Effect<string, Error>
// Retry a fixed number of times, immediately.const fixed = request.pipe(Effect.retry({ times: 3 }))
// Retry with exponential backoff — the idiomatic policy for network calls.const backoff = request.pipe( Effect.retry(Schedule.exponential("200 millis")))
// Retry only while a condition holds (e.g. only on transient errors).const conditional = request.pipe( Effect.retry({ times: 5, while: (error) => error.message.includes("timeout") }))When all retries are exhausted, the effect fails with the last error. To run a
recovery effect instead, use Effect.retryOrElse:
import { Effect, Schedule } from "effect"
declare const request: Effect.Effect<string, Error>
const withFallback = request.pipe( Effect.retryOrElse( Schedule.recurs(3), // Runs once retries are exhausted; receives the final error. (error) => Effect.succeed(`giving up: ${error.message}`) ))orElseSucceed & firstSuccessOf — fall back
Section titled “orElseSucceed & firstSuccessOf — fall back”Effect.orElseSucceed replaces any failure with a constant success value,
guaranteeing the effect completes.
import { Effect } from "effect"
declare const loadSetting: Effect.Effect<number, Error>
// Always yields a number; on failure, falls back to 8080.const port = loadSetting.pipe(Effect.orElseSucceed(() => 8080))// port: Effect<number, never>When you have several alternatives to try in order, Effect.firstSuccessOf
returns the first that succeeds, or the last error if all fail.
import { Effect } from "effect"
declare const fromCache: Effect.Effect<string, Error>declare const fromPrimary: Effect.Effect<string, Error>declare const fromBackup: Effect.Effect<string, Error>
// Try cache, then primary, then backup — short-circuiting on the first success.const value = Effect.firstSuccessOf([fromCache, fromPrimary, fromBackup])repeat — run again on success
Section titled “repeat — run again on success”Effect.repeat is the mirror image of retry: it re-runs an effect that
succeeds, according to a schedule. It’s the basis for polling and periodic
work.
import { Effect, Schedule } from "effect"
declare const poll: Effect.Effect<string, Error>
// Run `poll`, then repeat it 5 more times, spaced 1 second apart.const polled = poll.pipe( Effect.repeat({ schedule: Schedule.spaced("1 second"), times: 5 }))timeout — give up after a deadline
Section titled “timeout — give up after a deadline”Effect.timeout fails with a TimeoutError if the effect doesn’t finish in
time, interrupting the in-flight work.
import { Effect } from "effect"
declare const slowQuery: Effect.Effect<string, Error>
// Adds `Cause.TimeoutError` to the error channel.const bounded = slowQuery.pipe(Effect.timeout("2 seconds"))Two variants change what happens on timeout:
Effect.timeoutOptionsucceeds withOption.none()instead of failing, so the timeout is modelled as absence rather than an error.Effect.timeoutOrElseruns a fallback effect, as shown in the opening example.
import { Effect } from "effect"
declare const slowQuery: Effect.Effect<string, Error>
// Effect<Option<string>, Error> — None means "timed out"const maybe = slowQuery.pipe(Effect.timeoutOption("2 seconds"))Composing the patterns
Section titled “Composing the patterns”These combinators compose freely. A common production shape is retry with
backoff, bounded by an overall timeout, with a final fallback — exactly the
resilient example at the top of this page. Build resilience in layers, from
the innermost operation outward.
Next steps
Section titled “Next steps”- Dive deeper into retry/repeat policies in Scheduling.
- Fold the final outcome with Matching.