| name | effect-patterns-concurrency-getting-started |
| description | Effect-TS patterns for Concurrency Getting Started. Use when working with concurrency getting started in Effect-TS applications. |
Effect-TS Patterns: Concurrency Getting Started
This skill provides 3 curated Effect-TS patterns for concurrency getting started.
Use this skill when working on tasks related to:
- concurrency getting started
- Best practices in Effect-TS applications
- Real-world patterns and solutions
🟢 Beginner Patterns
Race Effects and Handle Timeouts
Rule: Use Effect.race for fastest-wins, Effect.timeout for time limits.
Good Example:
import { Effect, Option } from "effect"
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)
})
const slowOperation = Effect.gen(function* () {
yield* Effect.sleep("5 seconds")
return "Finally done"
})
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}`)
}
})
const withFallback = slowOperation.pipe(
Effect.timeoutTo({
duration: "1 second",
onTimeout: () => Effect.succeed("Using cached value"),
})
)
Effect.runPromise(withFallback).then((result) => {
console.log(result)
})
class TimeoutError {
readonly _tag = "TimeoutError"
}
const failOnTimeout = slowOperation.pipe(
Effect.timeoutFail({
duration: "1 second",
onTimeout: () => new TimeoutError(),
})
)
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}`)
})
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
})
Rationale:
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
Understanding Fibers
Rule: Fibers are lightweight threads managed by Effect, enabling efficient concurrency without OS thread overhead.
Good Example:
import { Effect, Fiber } from "effect"
const myEffect = Effect.gen(function* () {
yield* Effect.log("Hello from a fiber!")
yield* Effect.sleep("100 millis")
return 42
})
Effect.runPromise(myEffect)
const withFork = Effect.gen(function* () {
yield* Effect.log("Main fiber starting")
const fiber = yield* Effect.fork(
Effect.gen(function* () {
yield* Effect.log("Child fiber running")
yield* Effect.sleep("200 millis")
yield* Effect.log("Child fiber done")
return "child result"
})
)
yield* Effect.log("Main fiber continues immediately")
yield* Effect.sleep("100 millis")
yield* Effect.log("Main fiber waiting for child...")
const result = yield* Fiber.join(fiber)
yield* Effect.log(`Got result: ${result}`)
})
Effect.runPromise(withFork)
const fiberOps = Effect.gen(function* () {
const fiber = yield* Effect.fork(
Effect.gen(function* () {
yield* Effect.sleep("1 second")
return "done"
})
)
const poll = yield* Fiber.poll(fiber)
yield* Effect.log(`Poll result: ${poll}`)
const result = yield* Fiber.join(fiber)
yield* Effect.log(`Join result: ${result}`)
})
Rationale:
Fibers are Effect's lightweight threads. They're cheap to create (thousands are fine), automatically managed, and can be interrupted cleanly.
Unlike OS threads:
- Lightweight - Create thousands without performance issues
- Cooperative - Yield control at effect boundaries
- Interruptible - Can be cancelled cleanly
- Structured - Parent fibers manage children
Your First Parallel Operation
Rule: Use Effect.all with concurrency option to run independent effects in parallel.
Good Example:
import { Effect } from "effect"
const fetchUser = Effect.gen(function* () {
yield* Effect.sleep("100 millis")
return { id: 1, name: "Alice" }
})
const fetchProducts = Effect.gen(function* () {
yield* Effect.sleep("150 millis")
return [{ id: 1, name: "Widget" }, { id: 2, name: "Gadget" }]
})
const fetchCart = Effect.gen(function* () {
yield* Effect.sleep("80 millis")
return { items: 3, total: 99.99 }
})
const sequential = Effect.all([fetchUser, fetchProducts, fetchCart])
const parallel = Effect.all(
[fetchUser, fetchProducts, fetchCart],
{ concurrency: "unbounded" }
)
const limited = Effect.all(
[fetchUser, fetchProducts, fetchCart],
{ concurrency: 2 }
)
const demo = Effect.gen(function* () {
const start = Date.now()
const [user, products, cart] = yield* parallel
const elapsed = Date.now() - start
yield* Effect.log(`Fetched in ${elapsed}ms`)
yield* Effect.log(`User: ${user.name}`)
yield* Effect.log(`Products: ${products.length}`)
yield* Effect.log(`Cart total: $${cart.total}`)
})
Effect.runPromise(demo)
Rationale:
Use Effect.all with { concurrency: "unbounded" } to run independent effects in parallel. Without the option, effects run sequentially.
Parallel execution speeds up independent operations:
- Fetch multiple APIs - Get user, products, cart simultaneously
- Process files - Read multiple files at once
- Database queries - Run independent queries in parallel