Skip to content

Latest commit

 

History

History
116 lines (93 loc) · 3.68 KB

File metadata and controls

116 lines (93 loc) · 3.68 KB
title Control Repetition with Schedule
id control-repetition-with-schedule
skillLevel intermediate
applicationPatternId error-management
summary Use Schedule to create composable, stateful policies that define precisely how an effect should be repeated or retried.
tags
schedule
repeat
retry
polling
policy
recurs
exponential
rule
description
Use Schedule to create composable policies for controlling the repetition and retrying of effects.
related
retry-based-on-specific-errors
poll-for-status-until-task-completes
author effect_website
lessonOrder 2

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<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);