effect-patterns-concurrency
maintained by PaulJPhilp
name: effect-patterns-concurrency description: Effect-TS patterns for Concurrency. Use when working with concurrency in Effect-TS applications.
Effect-TS Patterns: Concurrency
This skill provides 20 curated Effect-TS patterns for concurrency. Use this skill when working on tasks related to:
- concurrency
- Best practices in Effect-TS applications
- Real-world patterns and solutions
🟡 Intermediate Patterns
Race Concurrent Effects for the Fastest Result
Rule: Use Effect.race to get the result from the first of several effects to succeed, automatically interrupting the losers.
Good Example:
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);
Anti-Pattern:
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]);
Rationale:
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.
Concurrency Pattern 2: Rate Limit Concurrent Access with Semaphore
Rule: Use Semaphore to limit concurrent access to resources, preventing overload and enabling fair resource distribution.
Good Example:
This example demonstrates limiting concurrent database connections using a Semaphore, preventing connection pool exhaustion.
import { Effect, Semaphore, Fiber } from "effect";
interfac
chat Comments (0)
Sign in to join the discussion and leave a comment.
Skill Details
Related Skills
Build your own?
Join 12,000+ developers contributing to the Claude ecosystem.
No comments yet. Be the first to share your thoughts!