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. By default the forked fiber begins executing
only after the current fiber yields (so don’t assume it has made progress the
instant forkChild returns). Pass { startImmediately: true } if you want it to
begin running right away.
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.
Fork variants (Effect)
Section titled “Fork variants (Effect)”Forking is the entry point to fibers, so the fork operators live on Effect.
All four return Effect<Fiber<A, E>, never, R> and accept the same
{ startImmediately?, uninterruptible? } options; they differ only in which
scope owns the child.
Effect.forkChild
Section titled “Effect.forkChild”Forks an effect into a child fiber supervised by the current fiber. When the current fiber ends, the child is interrupted. This is the structured-concurrency default.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { // Fork without blocking; the child starts after this fiber next yields. const fiber = yield* Effect.forkChild(Effect.succeed(42))
// Start immediately instead of waiting for the next yield point. const eager = yield* Effect.forkChild(Effect.succeed(1), { startImmediately: true })
return yield* Fiber.join(fiber) // => 42})
Effect.runFork(program)Effect.forkScoped
Section titled “Effect.forkScoped”Forks into the enclosing Scope rather than the current fiber, so the child
can outlive the forking fiber and is interrupted when the scope closes. Adds
Scope to the effect’s requirements.
import { Effect } from "effect"
const program = Effect.scoped( Effect.gen(function*() { // Lives until the surrounding `scoped` block (the Scope) closes. yield* Effect.forkScoped( Effect.forever(Effect.log("tick").pipe(Effect.delay("1 second"))) ) yield* Effect.sleep("2500 millis") // => logs "tick" ~twice, then the scope closes and the fiber is interrupted }))
Effect.runFork(program)Effect.forkIn
Section titled “Effect.forkIn”Forks into a specific Scope you captured earlier — the same as
forkScoped but you choose exactly which scope owns the fiber.
import { Effect } from "effect"
const task = Effect.gen(function*() { yield* Effect.sleep("10 seconds") return "completed"})
const program = Effect.scoped( Effect.gen(function*() { const scope = yield* Effect.scope const fiber = yield* Effect.forkIn(task, scope) yield* Effect.sleep("1 second") // `fiber` is interrupted when `scope` closes return fiber }))
Effect.runFork(program)Effect.forkDetach
Section titled “Effect.forkDetach”Forks into the global scope as a daemon. The child is not interrupted when the forking fiber ends — only when the runtime shuts down. Use it for long-lived background work that must escape its parent.
import { Effect } from "effect"
const daemon = Effect.forever( Effect.log("daemon running...").pipe(Effect.delay("1 second")))
const program = Effect.gen(function*() { yield* Effect.forkDetach(daemon) yield* Effect.log("daemon started") // The daemon keeps running after this fiber completes.})
Effect.runFork(program)Effect.withFiber
Section titled “Effect.withFiber”Gives the surrounding effect access to the current fiber so you can read its runtime fields (id, scheduler, references). The callback returns an effect.
import { Effect } from "effect"
const program = Effect.withFiber((fiber) => Effect.succeed(`Fiber ID: ${fiber.id}`))
Effect.runPromise(program).then(console.log)// => Fiber ID: 1Effect.fiber
Section titled “Effect.fiber”The current fiber as a plain value (Effect<Fiber<unknown, unknown>>), for when
you want to yield* it inside Effect.gen rather than take a callback.
import { Effect } from "effect"
const program = Effect.gen(function*() { const self = yield* Effect.fiber yield* Effect.log(`running on fiber ${self.id}`) // => running on fiber 1})
Effect.runFork(program)Effect.fiberId
Section titled “Effect.fiberId”The current fiber’s numeric id (Effect<number>). Handy for tagging logs and
spans.
import { Effect } from "effect"
const program = Effect.gen(function*() { const id = yield* Effect.fiberId return id // => 1})
Effect.runFork(program)Effect.awaitAllChildren
Section titled “Effect.awaitAllChildren”Delays the wrapped effect’s completion until all child fibers it forked have finished. Children that already existed before the wrapped effect started are not awaited.
import { Effect } from "effect"
const program = Effect.gen(function*() { yield* Effect.forkChild(Effect.sleep("1 second").pipe(Effect.andThen(Effect.log("child done")))) yield* Effect.log("parent body done")}).pipe(Effect.awaitAllChildren)// => "parent body done", then ~1s later "child done", then the effect completes
Effect.runFork(program)Fiber module reference
Section titled “Fiber module reference”The functions below operate on a Fiber<A, E> handle you already hold.
Fiber.await
Section titled “Fiber.await”Waits for a fiber to complete and returns its Exit — it never
fails, so use it to inspect the outcome (success, failure, interruption).
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const fiber = yield* Effect.forkChild(Effect.succeed(42)) const exit = yield* Fiber.await(fiber) return exit // => Exit.succeed(42)})
Effect.runFork(program)Fiber.join
Section titled “Fiber.join”Waits for a fiber and propagates its result into the current effect: success
returns the value, failure re-raises the fiber’s Cause.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const fiber = yield* Effect.forkChild(Effect.succeed(42)) const result = yield* Fiber.join(fiber) return result // => 42})
Effect.runFork(program)Fiber.awaitAll
Section titled “Fiber.awaitAll”Waits for every fiber in an iterable and returns an array of their Exit values,
in input order. Failures are captured as Exit.Failure rather than re-raised.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const f1 = yield* Effect.forkChild(Effect.succeed(1)) const f2 = yield* Effect.forkChild(Effect.succeed(2)) const exits = yield* Fiber.awaitAll([f1, f2]) return exits // => [Exit.succeed(1), Exit.succeed(2)]})
Effect.runFork(program)Fiber.joinAll
Section titled “Fiber.joinAll”Waits for every fiber to succeed and returns their values in input order. The
first failure fails the returned effect and stops waiting — but it does not
interrupt the remaining fibers (use Fiber.interruptAll for that).
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const f1 = yield* Effect.forkChild(Effect.succeed("a")) const f2 = yield* Effect.forkChild(Effect.succeed("b")) const values = yield* Fiber.joinAll([f1, f2]) return values // => ["a", "b"]})
Effect.runFork(program)Fiber.interrupt
Section titled “Fiber.interrupt”Requests cancellation of a fiber and resumes only after it has finished running its finalizers. Interruption is cooperative.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const fiber = yield* Effect.forkChild( Effect.delay("1 second")(Effect.succeed(42)) ) yield* Fiber.interrupt(fiber) return "interrupted" // => "interrupted"})
Effect.runFork(program)Fiber.interruptAs
Section titled “Fiber.interruptAs”Like interrupt, but records a specific interruptor fiber id in the cause —
useful for diagnostics and tracing. It does not change interruption semantics.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const target = yield* Effect.forkChild( Effect.delay("5 seconds")(Effect.succeed("task")) ) yield* Fiber.interruptAs(target, 123) // attributed to fiber #123 return "done"})
Effect.runFork(program)Fiber.interruptAll
Section titled “Fiber.interruptAll”Interrupts every fiber in an iterable, recording the current fiber as the interruptor. Resumes only after all of them have finished.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const f1 = yield* Effect.forkChild(Effect.sleep("5 seconds")) const f2 = yield* Effect.forkChild(Effect.sleep("3 seconds")) yield* Fiber.interruptAll([f1, f2]) return "all interrupted" // => "all interrupted"})
Effect.runFork(program)Fiber.interruptAllAs
Section titled “Fiber.interruptAllAs”Like interruptAll, but attributes the interruption to a specific fiber id.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const controller = yield* Effect.forkChild(Effect.succeed("controller")) const worker = yield* Effect.forkChild(Effect.sleep("5 seconds")) yield* Fiber.interruptAllAs([worker], controller.id) return "interrupted by controller"})
Effect.runFork(program)Fiber.isFiber
Section titled “Fiber.isFiber”Type guard that checks whether an unknown value is a Fiber.
import { Effect, Fiber } from "effect"
const program = Effect.gen(function*() { const fiber = yield* Effect.forkChild(Effect.succeed(42)) return [Fiber.isFiber(fiber), Fiber.isFiber("hello")] // => [true, false]})
Effect.runFork(program)Fiber.getCurrent
Section titled “Fiber.getCurrent”Synchronous accessor (not an effect) that returns the currently executing fiber,
or undefined outside a fiber runtime context. For low-level runtime
integrations; prefer Effect.withFiber / Effect.fiber in normal code.
import { Effect, Fiber } from "effect"
const program = Effect.sync(() => { const current = Fiber.getCurrent() return current?.id // => 1 (inside a fiber)})
Effect.runFork(program)Fiber.runIn
Section titled “Fiber.runIn”Registers a manually managed fiber with a Scope so it is interrupted when the
scope closes, and returns the same fiber. It does not wait for completion. If
the scope is already closed, the fiber is interrupted immediately.
import { Effect, Fiber } from "effect"
const program = Effect.scoped( Effect.gen(function*() { const scope = yield* Effect.scope const fiber = yield* Effect.forkDetach(Effect.sleep("10 seconds")) // Tie the detached fiber's lifetime back to this scope. Fiber.runIn(fiber, scope) return "registered" // fiber is interrupted when the scope closes }))
Effect.runFork(program)The Fiber interface
Section titled “The Fiber interface”Beyond the functions above, a Fiber<A, E> exposes runtime fields. Most code
never touches these — they exist for schedulers, tracing, and runtime
integrations. The unsafe methods bypass Effect’s sequencing guarantees; prefer
the module functions above.
id— the fiber’s numeric identifier.currentOpCount— operations executed since the last yield; the scheduler compares this againstmaxOpsBeforeYield.getRef(ref)— read aContext.Referencevalue for this fiber.context— the fiber’sContext(andsetContextto replace it).currentScheduler— theSchedulerdriving this fiber.addObserver(cb)— register a callback invoked with the fiber’sExitwhen it completes; returns a function that removes the observer.interruptUnsafe(fiberId?, annotations?)— immediately request interruption without Effect sequencing.pollUnsafe()— return the fiber’sExitif it has already completed, otherwiseundefined.
import { Effect } from "effect"
const program = Effect.gen(function*() { const fiber = yield* Effect.forkChild(Effect.succeed(42))
// Observe completion via a low-level callback. fiber.addObserver((exit) => { console.log(exit) // => Exit.succeed(42) })
// Has it finished yet? (likely undefined right after forking) fiber.pollUnsafe() // => undefined | Exit<42, never>})
Effect.runFork(program)Scheduler reference
Section titled “Scheduler reference”The scheduler is the runtime piece that decides how runnable tasks are enqueued, when they are dispatched, and whether a fiber should yield after using up its operation budget. This is advanced runtime tuning — you rarely provide a custom scheduler, but the references below let you tune fairness.
Scheduler
Section titled “Scheduler”The Scheduler interface, also exported as a Context.Reference so you can
provide a custom implementation. A scheduler has an executionMode
("sync" | "async"), a shouldYield(fiber) predicate, and makeDispatcher().
import { Effect } from "effect"import { Scheduler, MixedScheduler } from "effect/Scheduler"
// Run with a synchronous scheduler (drains work eagerly).const program = Effect.succeed(1).pipe( Effect.provideService(Scheduler, new MixedScheduler("sync")))
Effect.runFork(program)MixedScheduler
Section titled “MixedScheduler”The default scheduler. It batches queued tasks and dispatches them by priority
(FIFO within a priority), supports "sync" and "async" execution modes, and
yields a fiber once fiber.currentOpCount >= fiber.maxOpsBeforeYield.
import { MixedScheduler } from "effect/Scheduler"
const scheduler = new MixedScheduler("async") // the runtime defaultscheduler.executionMode // => "async"SchedulerDispatcher
Section titled “SchedulerDispatcher”Created by a scheduler via makeDispatcher(). scheduleTask(task, priority)
enqueues work; flush() drains pending work synchronously for deterministic
completion.
import { MixedScheduler } from "effect/Scheduler"
const dispatcher = new MixedScheduler().makeDispatcher()dispatcher.scheduleTask(() => console.log("ran"), 0)dispatcher.flush() // => logs "ran"MaxOpsBeforeYield
Section titled “MaxOpsBeforeYield”Context.Reference<number> controlling how many operations a fiber may perform
before the scheduler makes it yield. Default 2048. Lower it for more frequent
yielding (fairer to other fibers), raise it for throughput on CPU-bound work.
import { Effect } from "effect"import { MaxOpsBeforeYield } from "effect/Scheduler"
const program = Effect.succeed("work").pipe( Effect.provideService(MaxOpsBeforeYield, 256) // yield more often)
Effect.runFork(program)PreventSchedulerYield
Section titled “PreventSchedulerYield”Context.Reference<boolean> that, when true, makes the run loop skip
Scheduler.shouldYield entirely. Default false. Bypassing yields can improve
throughput for controlled workloads but lets long-running fibers monopolize the
JavaScript thread.
import { Effect } from "effect"import { PreventSchedulerYield } from "effect/Scheduler"
const program = Effect.succeed("hot loop").pipe( Effect.provideService(PreventSchedulerYield, true) // never yield cooperatively)
Effect.runFork(program)