Skip to main content

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

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