# Repetition & Retry

A [`Schedule`](https://effect.plants.sh/scheduling/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.retry`** re-runs an effect when it **fails**. The schedule's `Input`
  is the error, so you can inspect the failure to decide whether to keep going.
- **`Effect.repeat`** re-runs an effect when it **succeeds**. The schedule's
  `Input` is 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.

```ts
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.
**Let the input type be inferred:** Instead of `setInputType`, you can pass a *schedule builder* to `retry`. The
  builder receives a helper `$` that pins the input type to the effect's error,
  so predicates like `while` infer it automatically:

  ```ts
  fetchUser("user-123").pipe(
    Effect.retry(($) =>
      $(Schedule.spaced("1 second")).pipe(
        Schedule.while(({ input }) => input.retryable)
      )
    )
  )
  ```

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

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

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.

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

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

```ts
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
      })
  )
)
```
**retry does not catch defects:** `retry` only re-runs on *typed failures* — the values in an effect's error
  channel. Defects (thrown exceptions surfaced via `Effect.die`) and
  interruptions are never retried. Validate inputs and model recoverable
  conditions as typed errors so your retry policy actually applies to them. See
  [Error Management](https://effect.plants.sh/error-management/) for the distinction.

## Testing scheduled effects

Schedules realize their delays through the [`Clock`](https://effect.plants.sh/scheduling/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](https://effect.plants.sh/scheduling/clock/) for the full pattern.

## Operator reference

Every operator below consumes a `Schedule` (or schedule options) and threads the
schedule's `Error` and `Env` (requirements) into the resulting effect. Two
return-value rules apply throughout:

- **`retry` / `retryOrElse`** return the **effect's** success value `A` — the
  schedule only governs *when* to re-run on failure.
- **`repeat` / `schedule` / `scheduleFrom`** return the **schedule's** final
  `Output` (the recurrence count for `Schedule.recurs`, the last `Duration` for
  duration-based schedules, etc.) — the effect ran successfully, so its value is
  not what's interesting.

A schedule typed `Schedule<Output, Input, Error, Env>` contributes `Error` to
the result's error channel and `Env` to its requirements channel; if you write a
schedule with no extra failures or requirements those are `never`.

### Effect.retry

Re-runs `self` on each typed **failure** until the schedule stops; the schedule's
`Input` is the error. Accepts a `Schedule`, a schedule-builder function, or a
`Retry.Options` object (`{ schedule?, times?, while?, until? }`). Returns the
effect's success value. (`Effect.ts:4027`)

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

let attempts = 0
const flaky = Effect.suspend(() =>
  ++attempts < 3 ? Effect.fail("boom") : Effect.succeed("ok")
)

// Schedule form: retry up to 5 extra times, 100ms apart.
const a = Effect.retry(flaky, Schedule.spaced("100 millis").pipe(Schedule.both(Schedule.recurs(5))))
Effect.runPromise(a).then(console.log)
// => "ok"  (succeeds on the 3rd attempt)

// Options form: cap attempts and guard with a predicate on the error.
const b = Effect.retry(flaky, { times: 5, while: (e) => e === "boom" })
// => Effect<string, string>  (retries only while the error is "boom")

// Builder form: `$` pins the schedule Input to the effect's error type.
const c = flaky.pipe(
  Effect.retry(($) => $(Schedule.spaced("1 second")).pipe(Schedule.recurs(3)))
)
```

### Effect.retryOrElse

Like `retry`, but when the retry schedule is exhausted and the effect is still
failing, it runs a fallback `orElse(error, scheduleOutput)` instead of
propagating the failure. The fallback receives the last error and the schedule's
final output (e.g. the recurrence count). (`Effect.ts:4106`)

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

const request: Effect.Effect<string, "timeout"> = Effect.fail("timeout")

const program = request.pipe(
  Effect.retryOrElse(
    Schedule.recurs(2),
    (error, count) => Effect.succeed(`fell back after ${count} retries (${error})`)
  )
)
Effect.runPromise(program).then(console.log)
// => "fell back after 2 retries (timeout)"
```

### Effect.repeat

Re-runs `self` on each **success** until the schedule stops or the effect fails;
the schedule's `Input` is the success value. Accepts a `Schedule`, a
schedule-builder, or a `Repeat.Options` object (`{ schedule?, times?, while?,
until? }`) whose `while`/`until` receive the success value. Returns the
schedule's final `Output`. (`Effect.ts:7534`)

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

const tick = Effect.sync(() => Date.now())

// Schedule form: run, then repeat twice more (3 executions total).
const a = Effect.repeat(tick, Schedule.recurs(2))
Effect.runPromise(a).then(console.log)
// => 2   (the recurrence count returned by Schedule.recurs)

// Options form: repeat until a success value satisfies a predicate.
let n = 0
const counter = Effect.sync(() => ++n)
const b = Effect.repeat(counter, { until: (value) => value >= 3 })
Effect.runPromise(b).then(console.log)
// => runs until `counter` returns 3
```

### Effect.repeatOrElse

Like `repeat`, but if the effect (or a schedule step) fails before the schedule
completes, it runs a fallback `orElse(error, option)`. The second argument is an
`Option<Output>`: `Some` once at least one schedule step has run, `None` if it
failed on the very first attempt. The fallback must produce the schedule's
`Output` type, so the whole expression still resolves to a single result type.
(`Effect.ts:7599`)

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

let attempt = 0
const task = Effect.suspend(() => {
  attempt++
  return attempt <= 2 ? Effect.fail(`error ${attempt}`) : Effect.succeed("done")
})

const program = Effect.repeatOrElse(
  task,
  Schedule.recurs(3),
  (error, count) =>
    // The fallback must return the schedule Output (a number for `recurs`).
    Console.log(
      `stopped: ${error}, steps=${Option.getOrElse(count, () => 0)}`
    ).pipe(Effect.as(0))
)
Effect.runPromise(program).then(console.log)
// => 0   (fails on the first run, so count is None and the fallback returns 0)
```

### Effect.schedule

A thin alias for `repeat` that seeds the schedule with `undefined` (its `Input`
is `unknown`), so the schedule's decisions don't depend on the effect's value.
Returns the schedule's final `Output`. Use it when you just want "run this on a
cadence" without wiring up an input type. (`Effect.ts:7724`)

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

const job = Effect.log("running scheduled job")

const program = Effect.schedule(job, Schedule.recurs(2))
Effect.runPromise(program).then(console.log)
// => 2   (logs three times total, returns the recurrence count)
```

### Effect.scheduleFrom

Like `schedule`, but `initial` seeds the schedule *before* the first run, and
each success value is then fed back as the next schedule `Input`. This lets the
schedule's continuation decision depend on what the effect produced. Returns the
schedule's final `Output`. (`Effect.ts:7774`)

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

let value = 0
const produce = Effect.sync(() => ++value)

// Keep going while the latest produced value (the schedule Input) is below 3;
// seed the schedule with 0 before the first run.
const program = Effect.scheduleFrom(
  produce,
  0,
  Schedule.forever.pipe(
    Schedule.setInputType<number>(),
    Schedule.while(({ input }) => input < 3)
  )
)
Effect.runPromise(program).then(console.log)
// => runs until `produce` yields 3, feeding each value back into the schedule
```

### Inline options

Both `retry` and `repeat` accept an options object instead of a full `Schedule`,
covering the common cases without building a policy by hand:

- **`schedule`** — a base `Schedule` to drive the recurrence.
- **`times`** — cap the number of additional runs (like `Schedule.recurs`).
- **`while`** — keep going while the predicate holds (`true`).
- **`until`** — stop as soon as the predicate holds (`true`); the inverse of
  `while`.

For `retry` the predicates receive the **error**; for `repeat` they receive the
**success value**. Each predicate may return a `boolean` or an `Effect<boolean>`,
so the decision can itself be effectful.

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

declare const fetchPage: (n: number) => Effect.Effect<{ page: number; last: boolean }>

// Repeat paging until the server says it returned the last page.
const drain = fetchPage(0).pipe(
  Effect.repeat({ until: (result) => result.last })
)
// => Effect<{ page: number; last: boolean }, never>
```
**The schedule-builder `$` helper:** When you need a real `Schedule` *and* want predicates to infer the effect's
  error (for `retry`) or success (for `repeat`) type, pass a builder function
  instead of calling [`Schedule.setInputType`](https://effect.plants.sh/scheduling/schedule/) yourself.
  The builder's `$` argument wraps a schedule and pins its `Input` to the
  effect's channel, so `Schedule.while`/`until` predicates are fully typed:

  ```ts
  declare const job: Effect.Effect<number, "RetryMe">

  job.pipe(
    Effect.retry(($) =>
      $(Schedule.exponential("100 millis")).pipe(
        Schedule.while(({ input }) => input === "RetryMe")
      )
    )
  )
  ```

For schedule construction itself (backoff, combinators, predicates) see
[Schedule](https://effect.plants.sh/scheduling/schedule/). For time-based scheduling primitives see
[Clock](https://effect.plants.sh/scheduling/clock/) and [Cron](https://effect.plants.sh/scheduling/cron/), and for the
typed-failure vs defect distinction that governs what `retry` re-runs, see
[Error Management](https://effect.plants.sh/error-management/).