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") }))retry accepts either a Schedule directly or an options object.
The options object has these keys, all optional:
schedule— aSchedulecontrolling timing and stopping. Combine with the others to layer a recurrence limit or predicate on top of a delay policy.times— a maximum number of retries after the initial attempt (the source effect always runs once first;{ times: 3 }means up to four total runs).while— keep retrying only while this predicate returnstruefor the error. As soon as it returnsfalse, the current failure propagates. May return abooleanor anEffect<boolean>.until— the inverse: stop retrying once this predicate returnstrue. Also accepts anEffect<boolean>.
import { Effect, Schedule } from "effect"
class RateLimited { readonly _tag = "RateLimited" }class Fatal { readonly _tag = "Fatal" }
declare const call: Effect.Effect<string, RateLimited | Fatal>
// Combine a delay policy with a recurrence cap and a predicate.const tuned = call.pipe( Effect.retry({ schedule: Schedule.exponential("100 millis"), times: 5, // Stop immediately on a fatal error; keep retrying rate limits. until: (error) => error._tag === "Fatal" }))When all retries are exhausted, the effect fails with the last error. To run a
recovery effect instead, use Effect.retryOrElse. Its orElse callback receives
the final error and the schedule’s last output:
import { Effect, Schedule } from "effect"
declare const request: Effect.Effect<string, Error>
const withFallback = request.pipe( Effect.retryOrElse( Schedule.recurs(3), // (error, scheduleOutput) => fallback effect, run once retries are exhausted. (error, _out) => Effect.succeed(`giving up: ${error.message}`) ))For the full menu of timing policies (jitter, fixed windows, intersecting and unioning schedules), see Schedule and Repetition & Retry.
Falling back to an alternative
Section titled “Falling back to an alternative”There is no bare Effect.orElse / Effect.orElseFail in v4. The two fallback
primitives are orElseSucceed (replace failure with a constant) and
firstSuccessOf (try a list of effects in order). To run another effect on
failure, use Effect.catch and return the
alternative effect from the handler — that is the idiomatic “try plan B”.
orElseSucceed
Section titled “orElseSucceed”Replaces any failure with a constant success value, guaranteeing the effect completes. The fallback is evaluated lazily.
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))// => Effect<number, never>firstSuccessOf
Section titled “firstSuccessOf”Runs a sequence of effects and returns the first that succeeds, short-circuiting. If all fail, it fails with the error of the last effect.
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 — stops at the first success.const value = Effect.firstSuccessOf([fromCache, fromPrimary, fromBackup])// => Effect<string, Error>Effect.catch — return an alternative effect
Section titled “Effect.catch — return an alternative effect”When the fallback is itself effectful (or you want to inspect the error), catch the failure and return the next effect to try.
import { Effect } from "effect"
declare const fromPrimary: Effect.Effect<string, Error>declare const fromBackup: Effect.Effect<string, Error>
// On any failure of the primary, switch to the backup effect.const value = fromPrimary.pipe( Effect.catch((error) => { console.log(`primary failed: ${error.message}`) return fromBackup }))// => Effect<string, Error>See Catching Errors for the full family
(catch, catchTag, catchTags, catchCause, plus ignore / eventually).
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, and stops on the first failure. It’s the
basis for polling and periodic work. Like retry, it accepts a Schedule or an
options object with the same { schedule, times, while, until } shape (the
predicates apply to the success value here, not the error).
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 }))
// Repeat only while the value is still "pending".const untilReady = poll.pipe( Effect.repeat({ schedule: Schedule.spaced("500 millis"), while: (status) => status === "pending" }))Effect.repeatOrElse is the success-side analogue of retryOrElse: it runs a
fallback if repetition fails before the schedule completes. For more on building
repetition policies, see Repetition & Retry.
timeout — give up after a deadline
Section titled “timeout — give up after a deadline”Effect.timeout fails with a Cause.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"))// => Effect<string, Error | Cause.TimeoutError>Two variants change what happens on timeout:
timeoutOption
Section titled “timeoutOption”Succeeds with Option.none() instead of failing, modelling the timeout as
absence rather than an error. A failure before the deadline is still preserved.
import { Effect } from "effect"
declare const slowQuery: Effect.Effect<string, Error>
// None means "timed out"; Some(value) means it finished in time.const maybe = slowQuery.pipe(Effect.timeoutOption("2 seconds"))// => Effect<Option<string>, Error>timeoutOrElse
Section titled “timeoutOrElse”Runs a fallback effect when the deadline is reached. The source effect is interrupted before the lazily-created fallback runs.
import { Effect } from "effect"
declare const slowQuery: Effect.Effect<string, Error>
const withFallback = slowQuery.pipe( Effect.timeoutOrElse({ duration: "2 seconds", orElse: () => Effect.succeed("cached result") }))// => Effect<string, Error>Timing & scheduling helpers
Section titled “Timing & scheduling helpers”These combinators measure or shift an effect in time. The durations come from
Clock, never from Date.now, so they’re deterministic
under TestClock.
Effect.timed
Section titled “Effect.timed”Pairs an effect’s result with how long it took to run. The original success, failure, or interruption is preserved; only the success value is wrapped.
import { Duration, Effect } from "effect"
declare const work: Effect.Effect<string, Error>
const measured = Effect.gen(function* () { const [duration, value] = yield* Effect.timed(work) // duration: Duration.Duration, value: string console.log(`took ${Duration.toMillis(duration)}ms`) return value})// Effect.timed(work) => Effect<[Duration, string], Error>Effect.delay
Section titled “Effect.delay”Delays the start of an effect by a duration, then runs it normally.
import { Console, Effect } from "effect"
const program = Console.log("hi").pipe(Effect.delay("1 second"))// => waits 1 second, then logs "hi"Effect.sleep
Section titled “Effect.sleep”Suspends the current fiber for a duration without blocking a JavaScript thread — just a wait, with no value.
import { Console, Effect } from "effect"
const program = Effect.gen(function* () { yield* Console.log("Start") yield* Effect.sleep("2 seconds") yield* Console.log("End") // 2 seconds later})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”- Full retry/repeat policies and schedule combinators: Repetition & Retry and Schedule.
- Catching specific failures, plus
ignoreandeventually: Catching Errors. - Fold the final outcome with Matching.