Skip to content

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)

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.

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.join waits 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.await waits and returns an Exit value 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.

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)).

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)
StrategyTied toUse when
forkChildthe forking fiberthe default — the child is part of this unit of work
forkScopedthe surrounding Scopethe fiber should outlive the forking fiber but die with a resource scope
forkIna specific captured Scopeyou need to choose exactly which scope owns the fiber
forkDetachthe 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") })
})
)
}

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.

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.

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)

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)

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)

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)

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: 1

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)

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)

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)

The functions below operate on a Fiber<A, E> handle you already hold.

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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 against maxOpsBeforeYield.
  • getRef(ref) — read a Context.Reference value for this fiber.
  • context — the fiber’s Context (and setContext to replace it).
  • currentScheduler — the Scheduler driving this fiber.
  • addObserver(cb) — register a callback invoked with the fiber’s Exit when it completes; returns a function that removes the observer.
  • interruptUnsafe(fiberId?, annotations?) — immediately request interruption without Effect sequencing.
  • pollUnsafe() — return the fiber’s Exit if it has already completed, otherwise undefined.
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)

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.

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)

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 default
scheduler.executionMode // => "async"

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"

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)

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)