Fibers
A fiber is the running execution of an effect. Where an Effect<A, E, R> is
a lazy, immutable description of a computation, a Fiber<A, E> is a live
handle on one that has actually started. You get a fiber by forking an
effect; with that handle you can wait for the result, inspect how it finished,
or cancel it.
Notice that a Fiber has no Requirements type parameter — only success A
and error E. By the time an effect is running in a fiber, its requirements have
already been provided, so there is nothing left to inject.
import { Effect, Fiber } from "effect"
// A small CPU-bound computation we can run in the background.const fib = (n: number): Effect.Effect<number> => n < 2 ? Effect.succeed(n) : Effect.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b)
const program = Effect.gen(function*() { // `forkChild` starts `fib(10)` in a new fiber and hands us back the handle. // The fiber is a child of this one, so its lifetime is tied to ours. const fiber = yield* Effect.forkChild(fib(10))
// ...do other work concurrently here...
// `join` suspends until the fiber finishes and returns its value. // If the fiber had failed, that failure would be re-raised here. const result = yield* Fiber.join(fiber) yield* Effect.log(`fib(10) = ${result}`) // fib(10) = 55})
Effect.runFork(program)Forking
Section titled “Forking”Effect.forkChild(effect) returns an Effect<Fiber<A, E>> — running it starts a
new fiber and gives you its handle. The forked fiber begins executing only after
the current fiber yields (so don’t assume it has made progress the instant
forkChild returns).
The child is automatically supervised: it is bound to the fiber that forked it. When the parent completes — normally, by failure, or by interruption — any still-running children are interrupted and their finalizers run. This is what “structured concurrency” means in practice, and it is the default you almost always want.
Joining vs. awaiting
Section titled “Joining vs. awaiting”There are two ways to wait for a fiber, and they differ in how they treat failure:
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const fiber = yield* Effect.forkChild(Effect.fail("boom" as const))
// `Fiber.await` always succeeds with an Exit describing the outcome. // It never re-raises — use it to *inspect* success, failure, or interruption. const exit = yield* Fiber.await(fiber) yield* Effect.log(exit) // { _id: 'Exit', _tag: 'Failure', cause: { ... failure: 'boom' } }})
Effect.runFork(program)Fiber.joinwaits and re-raises the fiber’s failure into the joining fiber. Use it when the child’s result is part of your happy path and a failure should propagate.Fiber.awaitwaits and returns anExitvalue instead — it never fails. Use it when you want to handle whatever happened (success, failure, or interruption) yourself.
You can wait on several fibers at once with Fiber.joinAll and
Fiber.awaitAll.
Interrupting
Section titled “Interrupting”When you no longer need a fiber’s result, interrupt it. This immediately signals the fiber to stop, runs all of its finalizers to release resources, and completes once the fiber has terminated.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { // A fiber that logs forever, every 200ms. const fiber = yield* Effect.forkChild( Effect.forever(Effect.log("tick").pipe(Effect.delay("200 millis"))) )
yield* Effect.sleep("500 millis")
// Signal interruption and wait until the fiber has fully torn down. yield* Fiber.interrupt(fiber) yield* Effect.log("fiber stopped")})
Effect.runFork(program)Fiber.interrupt returns Effect<void> and, by default, back-pressures: it
does not resume until the target fiber has finished running its finalizers. That
guarantees no new work starts before the old work has cleaned up. If you’d
rather not wait, fork the interruption itself with
Effect.forkChild(Fiber.interrupt(fiber)).
Child fiber lifetimes
Section titled “Child fiber lifetimes”forkChild is one of four fork strategies. They differ only in what scope owns
the child — that is, when the child gets interrupted:
import { Effect } from "effect"
const task = Effect.forever(Effect.log("working").pipe(Effect.delay("1 second")))
// 1. Supervised by the parent fiber (the default, structured choice).// Interrupted when the forking fiber ends.const a = Effect.forkChild(task)
// 2. Bound to the surrounding Scope. The fiber can outlive the forking fiber// and is interrupted when the Scope closes. Adds `Scope` to requirements.const b = Effect.forkScoped(task)
// 3. Bound to a *specific* Scope you captured earlier — finer-grained control.const c = Effect.gen(function*() { const scope = yield* Effect.scope return yield* Effect.forkIn(task, scope)})
// 4. Detached: attached to the global scope, runs as a daemon. Not interrupted// when the forking fiber ends — only when the runtime shuts down.const d = Effect.forkDetach(task)| Strategy | Tied to | Use when |
| --- | --- | --- |
| forkChild | the forking fiber | the default — the child is part of this unit of work |
| forkScoped | the surrounding Scope | the fiber should outlive the forking fiber but die with a resource scope |
| forkIn | a specific captured Scope | you need to choose exactly which scope owns the fiber |
| forkDetach | the global scope (daemon) | a long-running background task that must survive its parent |
forkScoped is the typical choice for a background worker that a service owns
for as long as the service’s layer is alive:
import { Context, Effect, Layer } from "effect"
// A service that runs a background heartbeat for its whole lifetime.export class Heartbeat extends Context.Service<Heartbeat, { readonly tick: Effect.Effect<void>}>()("app/Heartbeat") { static readonly layer = Layer.effect( Heartbeat, Effect.gen(function*() { // `forkScoped` ties the worker to the layer's Scope: when the layer is // released, the worker fiber is interrupted automatically — no manual // bookkeeping, no leaked background fiber. yield* Effect.forkScoped( Effect.forever( Effect.log("heartbeat").pipe(Effect.delay("5 seconds")) ) )
return Heartbeat.of({ tick: Effect.log("manual tick") }) }) )}When you need many fibers
Section titled “When you need many fibers”Forking and joining a handful of fibers by hand is fine. Once you’re forking one
per element of a collection, prefer the
concurrency option on Effect.forEach /
Effect.all — it forks, supervises, and bounds parallelism for you. And when
you need to keep a dynamic set of fibers around (e.g. one per connection),
reach for the fiber collections:
FiberHandle, FiberMap, and FiberSet.