Supervisor
A Supervisor is a scope that tracks fibers you start through it. When the
supervisor's scope closes, it either cancels all still-running fibers
(the default) or waits for them to finish naturally — depending on how it
was created. This gives you a structured, leak-free way to run background work
without manually joining every fiber you start.
Supervisor is a Resource, so its lifecycle integrates naturally with the
rest of the resource graph: open it, use it, and let release handle cleanup.
Creating a Supervisor
| Constructor | Behavior on finalization |
|---|---|
Supervisor.create() | Cancels all active supervised fibers |
Supervisor.create(waitForAll: true) | Waits for all supervised fibers to complete |
Supervising a fiber
supervise(io) starts io as a fiber whose lifetime is bound to the
supervisor. It returns IO<IOFiber<A>>, so you can still join the fiber or
inspect its outcome if you need the result — but you don't have to.
IO<int> supervisorBasic() => Supervisor.create().use(
(supervisor) => supervisor.supervise(IO.pure(42)).flatMap((fiber) => fiber.joinWithNever()),
);
Fire-and-forget background work
The most common use of Supervisor is starting work you don't need to join.
The supervised fiber runs concurrently; when the use block exits the
supervisor cancels it automatically.
/// Supervised fibers don't need to be joined.
/// When the supervisor's scope closes, all still-running fibers are canceled.
IO<Unit> fireAndForget() => Supervisor.create().use((supervisor) {
return supervisor.supervise(IO.sleep(1.seconds).productR(() => IO.print('done'))).voided();
});
Waiting for fibers on shutdown
Pass waitForAll: true when you want finalization to drain in-flight work
rather than cancel it — useful for tasks like flushing a write buffer or
completing an in-progress unit of work before the application exits.
/// With waitForAll=true, finalization blocks until every supervised fiber
/// completes naturally rather than canceling them.
IO<Unit> supervisorWaitForAll() => IO.ref(false).flatMap((completed) {
return Supervisor.create(waitForAll: true)
.use((supervisor) {
return supervisor
.supervise(
IO.sleep(200.milliseconds).productR(() => completed.setValue(true)),
)
.voided();
})
.productR(() => completed.value())
.flatMap((v) => IO.print('completed: $v')); // completed: true
});
Real-world example: background health-check loop
A common pattern is to run a periodic side-effect (health-check, metric flush, cache refresh) alongside your main logic, stopping it cleanly when the surrounding resource scope closes.
withHealthCheck below wraps a Supervisor in a Resource. The check loop
runs forever in a supervised fiber; releasing the resource cancels the loop
without any manual fiber tracking.
/// Attaches a periodic health-check to a [Resource] scope.
///
/// The check runs in a background fiber supervised by [Supervisor].
/// When the [Resource] is released the [Supervisor] cancels the loop
/// automatically — no manual fiber management required.
Resource<Unit> withHealthCheck(IO<Unit> check, Duration interval) {
final loop = check.productR(() => IO.sleep(interval)).foreverM();
return Supervisor.create().flatMap(
(supervisor) => Resource.eval(supervisor.supervise(loop).voided()),
);
}
IO<Unit> healthCheckExample() => IO.ref(0).flatMap((counter) {
final check = counter
.update((n) => n + 1)
.productR(
() => counter.value().flatMap((n) => IO.print('check #$n')),
);
return withHealthCheck(check, 100.milliseconds)
.use((_) => IO.sleep(350.milliseconds))
.productR(() => counter.value())
.flatMap((n) => IO.print('ran $n checks'));
// prints: check #1, check #2, check #3, ran 3 checks
});
withHealthCheck can be combined with any other Resource using flatMap
or Dart's record-tuple syntax, so it slots into a larger resource graph
without extra boilerplate.