Skip to content

Fiber Collections

Effect.forkChild is perfect for a fixed handful of fibers, but real services spawn fibers dynamically — one per WebSocket connection, one per scheduled job, one per in-flight subscription. You need somewhere to keep those handles so they can be interrupted when the owning scope closes, and so a fiber is forgotten once it finishes. That’s what the fiber collections do:

  • FiberHandle — holds at most one fiber. Starting a new one interrupts the previous. Great for “latest wins” work like a debounced refresh.
  • FiberMap — holds fibers keyed by some value. Ideal for one fiber per entity (connection id, user id, job id).
  • FiberSet — holds an unkeyed set of fibers. Use it for a pool of workers you start and forget.

All three are created within a Scope. When that scope closes, every fiber in the collection is interrupted automatically — so they fit naturally inside a service’s layer, and you never leak a background fiber.

import { Context, Effect, FiberMap, Layer } from "effect"
// A service that runs one background subscription fiber per topic.
export class Subscriptions extends Context.Service<Subscriptions, {
subscribe(topic: string): Effect.Effect<void>
unsubscribe(topic: string): Effect.Effect<void>
}>()("app/Subscriptions") {
static readonly layer = Layer.effect(
Subscriptions,
Effect.gen(function*() {
// The FiberMap is tied to this layer's Scope: when the layer is
// released, every subscription fiber is interrupted for us.
const fibers = yield* FiberMap.make<string>()
const subscribe = Effect.fn("Subscriptions.subscribe")(
function*(topic: string) {
// `run` forks the effect and stores it under `topic`. If a fiber
// already exists for that key it is interrupted and replaced.
yield* FiberMap.run(
fibers,
topic,
Effect.forever(
Effect.log(`polling ${topic}`).pipe(Effect.delay("1 second"))
)
)
}
)
const unsubscribe = Effect.fn("Subscriptions.unsubscribe")(
function*(topic: string) {
// Interrupt and drop just this key's fiber.
yield* FiberMap.remove(fibers, topic)
}
)
return Subscriptions.of({ subscribe, unsubscribe })
})
)
}

You could keep fibers in a Set yourself, but you’d have to remove each one when it completes, interrupt every survivor on shutdown, and handle the race between a fiber finishing and you removing it. The fiber collections do all of this correctly:

  • a fiber is removed automatically when it terminates, so the collection never holds dead handles;
  • closing the owning scope interrupts all remaining fibers;
  • failures can be propagated to the collection so one bad fiber can surface an error instead of failing silently.

A FiberHandle stores a single fiber. FiberHandle.run forks an effect into it; if it already holds a running fiber, that one is interrupted first. This is the “only the latest matters” pattern — re-running a search as the user types, or restarting a watcher when config changes.

import { Effect, FiberHandle } from "effect"
const program = Effect.scoped(
Effect.gen(function*() {
const handle = yield* FiberHandle.make<string>()
// Start a task...
yield* FiberHandle.run(handle, Effect.succeed("v1").pipe(Effect.delay("1 second")))
// ...then start a newer one. The previous fiber is interrupted.
yield* FiberHandle.run(handle, Effect.succeed("v2"))
// `join` waits for the current fiber and re-raises its failure, if any.
yield* FiberHandle.join(handle)
})
)

Pass { onlyIfMissing: true } to run if you’d rather keep the existing fiber and skip starting a new one when one is already present.

A FiberMap<K> indexes fibers by a key. FiberMap.run(map, key, effect) forks under that key (replacing any existing fiber for it), FiberMap.remove(map, key) interrupts and drops one, and FiberMap.has / FiberMap.size let you inspect the collection. This is the workhorse for “one background fiber per entity” — see the Subscriptions service above.

A FiberSet is the simplest of the three: an unkeyed bag of fibers. Use it to fork a fluctuating number of tasks and have them all cleaned up together.

import { Effect, FiberSet } from "effect"
const program = Effect.scoped(
Effect.gen(function*() {
const set = yield* FiberSet.make<void>()
// Fork some work into the set; each fiber removes itself when it finishes.
yield* FiberSet.run(set, Effect.log("job a").pipe(Effect.delay("100 millis")))
yield* FiberSet.run(set, Effect.log("job b").pipe(Effect.delay("200 millis")))
// Wait until every fiber in the set has completed.
yield* FiberSet.awaitEmpty(set)
})
)
// Leaving the scope would interrupt any fibers still running.

| Need | Use | | --- | --- | | Only the most recent task should run | FiberHandle | | One fiber per id / entity, addressable later | FiberMap | | A pool of interchangeable background tasks | FiberSet | | A fixed, known number of fibers in a gen block | plain forkChild + Fiber.join | | Map a collection with a parallelism cap | the concurrency option |