EffectTalk
Back to Tour
error-managementIntermediate

Control Repetition with Schedule

Use Schedule to create composable, stateful policies that define precisely how an effect should be repeated or retried.

Guideline

A Schedule<In, Out> is a highly-composable blueprint that defines a recurring schedule. It takes an input of type In (e.g., the error from a failed effect) and produces an output of type Out (e.g., the decision to continue). Use Schedule with operators like Effect.repeat and Effect.retry to control complex repeating logic.


Rationale

While you could write manual loops or recursive functions, Schedule provides a much more powerful, declarative, and composable way to manage repetition. The key benefits are:

  • Declarative: You separate the what (the effect to run) from the how and when (the schedule it runs on).
  • Composable: You can build complex schedules from simple, primitive ones. For example, you can create a schedule that runs "up to 5 times, with an exponential backoff, plus some random jitter" by composing Schedule.recurs, Schedule.exponential, and Schedule.jittered.
  • Stateful: A Schedule keeps track of its own state (like the number of repetitions), making it easy to create policies that depend on the execution history.

Good Example

This example demonstrates composition by creating a common, robust retry policy: exponential backoff with jitter, limited to 5 attempts.

import { Effect, Schedule, Duration } from "effect";

// A simple effect that can fail
const flakyEffect = Effect.try({
  try: () => {
    if (Math.random() > 0.2) {
      throw new Error("Transient error");
    }
    return "Operation succeeded!";
  },
  catch: (error: unknown) => {
    Effect.logInfo("Operation failed, retrying...");
    return error;
  },
});

// --- Building a Composable Schedule ---

// 1. Start with a base exponential backoff (100ms, 200ms, 400ms...)
const exponentialBackoff = Schedule.exponential("100 millis");

// 2. Add random jitter to avoid thundering herd problems
const withJitter = Schedule.jittered(exponentialBackoff);

// 3. Limit the schedule to a maximum of 5 repetitions
const limitedWithJitter = Schedule.compose(withJitter, Schedule.recurs(5));

// --- Using the Schedule ---
const program = Effect.gen(function* () {
  yield* Effect.logInfo("Starting operation...");
  const result = yield* Effect.retry(flakyEffect, limitedWithJitter);
  yield* Effect.logInfo(`Final result: ${result}`);
});

// Run the program
Effect.runPromise(program);

Anti-Pattern

Writing manual, imperative retry logic. This is verbose, stateful, hard to reason about, and not easily composable.

import { Effect } from "effect";
import { flakyEffect } from "./somewhere";

// ❌ WRONG: Manual, stateful, and complex retry logic.
function manualRetry(
  effect: typeof flakyEffect,
  retriesLeft: number,
  delay: number
): Effect.Effect&#x3C;string, "ApiError"> {
  return effect.pipe(
    Effect.catchTag("ApiError", () => {
      if (retriesLeft > 0) {
        return Effect.sleep(delay).pipe(
          Effect.flatMap(() => manualRetry(effect, retriesLeft - 1, delay * 2))
        );
      }
      return Effect.fail("ApiError" as const);
    })
  );
}

const program = manualRetry(flakyEffect, 5, 100);
Control Repetition with Schedule | EffectTalk | EffectTalk