# 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](https://effect.plants.sh/scheduling/schedule/).

```ts
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

`Effect.retry` re-runs an effect when it fails. The simplest form just bounds the
number of attempts; pass a [`Schedule`](https://effect.plants.sh/scheduling/schedule/) to control the
timing.

```ts
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` — a `Schedule` controlling 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 returns `true` for the
  error. As soon as it returns `false`, the current failure propagates. May
  return a `boolean` or an `Effect<boolean>`.
- `until` — the inverse: stop retrying *once* this predicate returns `true`.
  Also accepts an `Effect<boolean>`.

```ts
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"
  })
)
```
**Note:** When `while` / `until` use a *type guard* (a `Refinement`), `retry` narrows the
  resulting error channel accordingly. Defects and interruptions are never
  retried.

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:

```ts
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](https://effect.plants.sh/scheduling/schedule/) and
[Repetition & Retry](https://effect.plants.sh/scheduling/repetition-and-retry/).

## 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`](https://effect.plants.sh/error-management/catching-errors/) and return the
alternative effect from the handler — that is the idiomatic "try plan B".

### orElseSucceed

Replaces *any* failure with a constant success value, guaranteeing the effect
completes. The fallback is evaluated lazily.

```ts
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

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.

```ts
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>
```
**Caution:** If given an **empty** collection, `firstSuccessOf` dies (defect) with an
  `Error` whose message is `"Received an empty collection of effects"`. The
  failure surfaces only when the effect is run — always pass at least one effect.

### 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.

```ts
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](https://effect.plants.sh/error-management/catching-errors/) for the full family
(`catch`, `catchTag`, `catchTags`, `catchCause`, plus `ignore` / `eventually`).

## 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).

```ts
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](https://effect.plants.sh/scheduling/repetition-and-retry/).

## 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.

```ts
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

Succeeds with `Option.none()` instead of failing, modelling the timeout as
*absence* rather than an error. A failure before the deadline is still preserved.

```ts
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

Runs a fallback effect when the deadline is reached. The source effect is
interrupted before the lazily-created fallback runs.

```ts
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>
```
**Caution:** `Effect.timeout` adds `Cause.TimeoutError` to the error channel. Handle it like
  any other tagged error, e.g. with
  [`Effect.catchTag("TimeoutError", ...)`](https://effect.plants.sh/error-management/catching-errors/).

## Timing & scheduling helpers

These combinators measure or shift an effect in time. The durations come from
[`Clock`](https://effect.plants.sh/scheduling/clock/), never from `Date.now`, so they're deterministic
under `TestClock`.

### 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.

```ts
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

Delays the *start* of an effect by a duration, then runs it normally.

```ts
import { Console, Effect } from "effect"

const program = Console.log("hi").pipe(Effect.delay("1 second"))
// => waits 1 second, then logs "hi"
```

### Effect.sleep

Suspends the current fiber for a duration without blocking a JavaScript thread —
just a wait, with no value.

```ts
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

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.
**Tip:** Retry and timeout durations come from [`Clock`](https://effect.plants.sh/scheduling/clock/), never from
  `Date.now`. In tests, drive them deterministically with `TestClock` — see
  [Testing](https://effect.plants.sh/testing/).

## Next steps

- Full retry/repeat policies and schedule combinators: [Repetition & Retry](https://effect.plants.sh/scheduling/repetition-and-retry/) and [Schedule](https://effect.plants.sh/scheduling/schedule/).
- Catching specific failures, plus `ignore` and `eventually`: [Catching Errors](https://effect.plants.sh/error-management/catching-errors/).
- Fold the final outcome with [Matching](https://effect.plants.sh/error-management/matching/).