Retry and Schedule
Retry with Exponential Backoff
import { Effect, Schedule, Console } from "effect"
let attempts = 0
const unstableCall = Effect.gen(function*() {
attempts++
yield* Console.log(`Attempt ${attempts}`)
if (attempts < 3) {
return yield* Effect.fail("not ready")
}
return "success"
})
const program = unstableCall.pipe(
Effect.retry(Schedule.exponential("1 seconds").pipe(Schedule.jittered))
)
Effect.runPromise(program).then(console.log)
// Output:
// Attempt 1
// Attempt 2
// Attempt 3
// success
Schedule.exponential doubles the delay after each failure. Schedule.jittered adds randomness to prevent thundering herd retries. The combined schedule waits ~1s, ~2s, ~4s (with jitter).
Retry with a Fixed Number of Times
const program = unstableCall.pipe(
Effect.retry(Schedule.recurs(5))
)
Schedule.recurs(5) retries up to 5 times with no delay between attempts. Combine with a delay schedule for paced retries:
const program = unstableCall.pipe(
Effect.retry(
Schedule.spaced("500 millis").pipe(Schedule.compose(Schedule.recurs(5)))
)
)
HTTP Retry with Backoff and Max Attempts
import { Effect, Schedule, Schema } from "effect"
class HttpError extends Schema.TaggedError<HttpError>()("HttpError", {
status: Schema.Number
}) {}
const fetchWithRetry = (url: string) =>
Effect.tryPromise({
try: () => fetch(url).then((r) => {
if (r.status >= 500) throw new HttpError({ status: r.status })
return r.json()
}),
catch: (e) => e instanceof HttpError ? e : new HttpError({ status: 0 })
}).pipe(
Effect.retry(
Schedule.exponential("1 seconds")
.pipe(Schedule.jittered)
.pipe(Schedule.compose(Schedule.recurs(3)))
),
Effect.catchTag("HttpError", (err) =>
Effect.succeed({ error: `Failed after retries: HTTP ${err.status}` })
)
)
Effect.runPromise(fetchWithRetry("https://api.example.com/data"))
This retries up to 3 times with exponential backoff (1s, 2s, 4s) plus jitter, then catches the final failure if all retries are exhausted.
Cron-Based Recurring Task
import { Effect, Schedule, Console } from "effect"
const heartbeat = Effect.gen(function*() {
yield* Console.log("Heartbeat tick")
})
const schedule = Schedule.cron("*/5 * * * *")
const program = heartbeat.pipe(
Effect.repeat(schedule)
)
Effect.runFork(program)
Schedule.cron accepts standard cron expressions. The example runs every 5 minutes. Use Effect.repeat to run an effect on the schedule.
Gotcha:
Schedule.cronrequires thecronfield format: minute, hour, day of month, month, day of week. Not all cron implementations are identical. Check the Effect cron parser for supported syntax.
Compose Schedule Constraints
import { Effect, Schedule, Duration, Console } from "effect"
const task = Console.log("Running task")
// Run every 10 seconds, but stop after 5 minutes total
const schedule = Schedule.spaced("10 seconds").pipe(
Schedule.compose(Schedule.duration(Duration.minutes(5)))
)
// Run with exponential backoff, max 30s delay, max 10 retries
const retrySchedule = Schedule.exponential("1 seconds").pipe(
Schedule.compose(Schedule.max("30 seconds")),
Schedule.compose(Schedule.recurs(10))
)
const program = task.pipe(Effect.repeat(schedule))
const retryProgram = unstableCall.pipe(
Effect.retry(retrySchedule)
)
Schedule.compose chains schedule constraints. Each constraint filters or transforms the delay pattern:
| Combinator | Effect |
|---|---|
Schedule.recurs(n) | Stop after n iterations |
Schedule.duration(d) | Stop after total elapsed time |
Schedule.max(d) | Cap the delay at d |
Schedule.jittered | Add random jitter to delays |
Schedule.spaced(d) | Fixed delay between iterations |
Schedule.exponential(d) | Double the delay each iteration |
Using Schedule with Effect.repeat
import { Effect, Schedule, Console } from "effect"
let count = 0
const poll = Effect.gen(function*() {
count++
yield* Console.log(`Poll ${count}`)
return count
})
// Repeat 4 times with 1 second spacing
const program = poll.pipe(
Effect.repeat(Schedule.spaced("1 seconds").pipe(Schedule.compose(Schedule.recurs(4))))
)
Effect.runPromise(program).then(console.log)
// Output (over ~4 seconds):
// Poll 1
// Poll 2
// Poll 3
// Poll 4
// 4
Effect.repeat runs the effect on the schedule and returns the final value. The schedule controls timing and termination.
Next Steps
- Handle Errors - Error recovery patterns
- Test Effect Programs - Test time-based logic with TestClock