| title |
Race Effects and Handle Timeouts |
| id |
concurrency-race-timeout |
| skillLevel |
beginner |
| applicationPatternId |
concurrency-getting-started |
| summary |
Race multiple effects to get the fastest result, or add timeouts to prevent hanging operations. |
| tags |
concurrency |
race |
timeout |
getting-started |
|
| rule |
| description |
Use Effect.race for fastest-wins, Effect.timeout for time limits. |
|
| author |
PaulJPhilp |
| related |
concurrency-hello-world |
concurrency-fork-basics |
|
| lessonOrder |
1 |
Use Effect.race when you want the first result from competing effects. Use Effect.timeout to limit how long an effect can run.
Racing and timeouts prevent your app from hanging:
- Redundant requests - Race multiple servers, use fastest response
- Timeouts - Fail fast if operation takes too long
- Fallbacks - Try fast path, fall back to slow path
import { Effect, Option } from "effect"
// ============================================
// BASIC RACE: First one wins
// ============================================
const server1 = Effect.gen(function* () {
yield* Effect.sleep("100 millis")
return "Response from server 1"
})
const server2 = Effect.gen(function* () {
yield* Effect.sleep("50 millis")
return "Response from server 2"
})
const raceServers = Effect.race(server1, server2)
Effect.runPromise(raceServers).then((result) => {
console.log(result) // "Response from server 2" (faster)
})
// ============================================
// BASIC TIMEOUT: Limit execution time
// ============================================
const slowOperation = Effect.gen(function* () {
yield* Effect.sleep("5 seconds")
return "Finally done"
})
// Returns Option.none if timeout
const withTimeout = slowOperation.pipe(
Effect.timeout("1 second")
)
Effect.runPromise(withTimeout).then((result) => {
if (Option.isNone(result)) {
console.log("Operation timed out")
} else {
console.log(`Got: ${result.value}`)
}
})
// ============================================
// TIMEOUT WITH FALLBACK
// ============================================
const withFallback = slowOperation.pipe(
Effect.timeoutTo({
duration: "1 second",
onTimeout: () => Effect.succeed("Using cached value"),
})
)
Effect.runPromise(withFallback).then((result) => {
console.log(result) // "Using cached value"
})
// ============================================
// TIMEOUT FAIL: Throw error on timeout
// ============================================
class TimeoutError {
readonly _tag = "TimeoutError"
}
const failOnTimeout = slowOperation.pipe(
Effect.timeoutFail({
duration: "1 second",
onTimeout: () => new TimeoutError(),
})
)
// ============================================
// RACE ALL: Multiple competing effects
// ============================================
const fetchFromCache = Effect.gen(function* () {
yield* Effect.sleep("10 millis")
return { source: "cache", data: "cached data" }
})
const fetchFromDB = Effect.gen(function* () {
yield* Effect.sleep("100 millis")
return { source: "db", data: "fresh data" }
})
const fetchFromAPI = Effect.gen(function* () {
yield* Effect.sleep("200 millis")
return { source: "api", data: "api data" }
})
const raceAll = Effect.raceAll([fetchFromCache, fetchFromDB, fetchFromAPI])
Effect.runPromise(raceAll).then((result) => {
console.log(`Winner: ${result.source}`) // "cache"
})
// ============================================
// PRACTICAL: API with timeout and fallback
// ============================================
const fetchWithResilience = (url: string) =>
Effect.gen(function* () {
const response = yield* Effect.tryPromise(() =>
fetch(url).then((r) => r.json())
).pipe(
Effect.timeout("3 seconds"),
Effect.flatMap((opt) =>
Option.isSome(opt)
? Effect.succeed(opt.value)
: Effect.succeed({ error: "timeout", cached: true })
)
)
return response
})
| Method |
Behavior |
Effect.timeout(duration) |
Returns Option<A>, None on timeout |
Effect.timeoutTo({duration, onTimeout}) |
Custom fallback effect |
Effect.timeoutFail({duration, onTimeout}) |
Fail with error on timeout |
| Method |
Behavior |
Effect.race(a, b) |
First of two effects |
Effect.raceAll([a, b, c]) |
First of many effects |
Effect.raceFirst(a, b) |
First to complete (even if error) |
// Try fast, fall back to slow
Effect.race(
fastPath.pipe(Effect.timeout("100 millis")),
slowPath
)
// Request with hard timeout
fetch(url).pipe(
Effect.timeoutFail({
duration: "5 seconds",
onTimeout: () => new RequestTimeoutError()
})
)
// Redundant requests for reliability
Effect.raceAll([
fetchFromServer("us-east"),
fetchFromServer("us-west"),
fetchFromServer("eu-west"),
])