| title | Race Concurrent Effects for the Fastest Result | |||||
|---|---|---|---|---|---|---|
| id | race-concurrent-effects | |||||
| skillLevel | intermediate | |||||
| applicationPatternId | concurrency | |||||
| summary | Use Effect.race to run multiple effects concurrently and proceed with the result of the one that succeeds first, automatically interrupting the others. | |||||
| tags |
|
|||||
| rule |
|
|||||
| related |
|
|||||
| author | effect_website | |||||
| lessonOrder | 10 |
When you have multiple effects that can produce the same type of result, and you only care about the one that finishes first, use Effect.race(effectA, effectB).
Effect.race is a powerful concurrency primitive for performance and resilience. It starts all provided effects in parallel. The moment one of them succeeds, Effect.race immediately interrupts all the other "losing" effects and returns the winning result. If one of the effects fails before any have succeeded, the race is not over; the remaining effects continue to run. The entire race only fails if all participating effects fail.
This is commonly used for:
- Performance: Querying multiple redundant data sources (e.g., two API replicas) and taking the response from whichever is faster.
- Implementing Timeouts: Racing a primary effect against a delayed
Effect.fail, effectively creating a timeout mechanism.
A classic use case is checking a fast cache before falling back to a slower database. We can race the cache lookup against the database query.
import { Effect, Option } from "effect";
type User = { id: number; name: string };
// Simulate a slower cache lookup that might find nothing (None)
const checkCache: Effect.Effect<Option.Option<User>> = Effect.succeed(
Option.none()
).pipe(
Effect.delay("200 millis") // Made slower so database wins
);
// Simulate a faster database query that will always find the data
const queryDatabase: Effect.Effect<Option.Option<User>> = Effect.succeed(
Option.some({ id: 1, name: "Paul" })
).pipe(
Effect.delay("50 millis") // Made faster so it wins the race
);
// Race them. The database should win and return the user data.
const program = Effect.race(checkCache, queryDatabase).pipe(
// The result of the race is an Option, so we can handle it.
Effect.flatMap((result: Option.Option<User>) =>
Option.match(result, {
onNone: () => Effect.fail("User not found anywhere."),
onSome: (user) => Effect.succeed(user),
})
)
);
// In this case, the database wins the race.
const programWithResults = Effect.gen(function* () {
try {
const user = yield* program;
yield* Effect.log(`User found: ${JSON.stringify(user)}`);
return user;
} catch (error) {
yield* Effect.logError(`Error: ${error}`);
throw error;
}
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Handled error: ${error}`);
return null;
})
)
);
Effect.runPromise(programWithResults);
// Also demonstrate with logging
const programWithLogging = Effect.gen(function* () {
yield* Effect.logInfo("Starting race between cache and database...");
try {
const user = yield* program;
yield* Effect.logInfo(
`Success: Found user ${user.name} with ID ${user.id}`
);
return user;
} catch (error) {
yield* Effect.logInfo("This won't be reached due to Effect error handling");
return null;
}
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logInfo(`Handled error: ${error}`);
return null;
})
)
);
Effect.runPromise(programWithLogging);Don't use Effect.race if you need the results of all the effects. That is the job of Effect.all. Using race in this scenario will cause you to lose data, as all but one of the effects will be interrupted and their results discarded.
import { Effect } from "effect";
const fetchProfile = Effect.succeed({ name: "Paul" });
const fetchPermissions = Effect.succeed(["admin", "editor"]);
// ❌ WRONG: This will only return either the profile OR the permissions,
// whichever resolves first. You will lose the other piece of data.
const incompleteData = Effect.race(fetchProfile, fetchPermissions);
// ✅ CORRECT: Use Effect.all when you need all the results.
const completeData = Effect.all([fetchProfile, fetchPermissions]);