What Are Fibers? — Lightweight Structured Tasks

A Fiber is an effect-managed async task. It's lighter than an OS thread and safer than a raw tokio::spawn.

Fibers vs. Raw Tasks

#![allow(unused)]
fn main() {
// Raw tokio::spawn — fire and forget
// Who owns this? What happens if it panics?
// When does it stop? Who cleans up?
tokio::spawn(async {
    do_something().await;
});

// Effect Fiber — explicit lifecycle
let handle: FiberHandle<Result, Error> = my_effect.fork();
// You hold the handle. The fiber is yours.
let exit: Exit<Error, Result> = handle.join().await;
// The fiber stops when you join or drop the handle.
}

With tokio::spawn, the task runs independently. If it panics, the panic is captured by Tokio and may or may not surface to you. There's no built-in way to cancel it or guarantee its resources are cleaned up.

With fork, you get a FiberHandle. When you join(), you get the full Exit — success, typed failure, panic, or cancellation. When you drop the handle without joining, the fiber is cancelled automatically.

Structured Concurrency

The key property of Fibers is structured lifecycle:

  • A fiber cannot outlive its parent scope without explicit permission
  • All spawned fibers are joined (or cancelled) before the parent effect completes
  • Panics and failures propagate through the fiber tree, not silently into the void

This makes concurrent code much easier to reason about. When process_batch completes, all its helper fibers have completed too — or been cancelled and cleaned up.

FiberId

Each Fiber has a unique FiberId. You can use it for logging, tracing, and correlation:

#![allow(unused)]
fn main() {
use id_effect::FiberId;

effect! {
    let id = ~ current_fiber_id();
    ~ log(&format!("[fiber:{id}] starting work"));
    // ...
}
}

FiberId flows through the fiber's execution automatically. You don't thread it manually.

FiberHandle and FiberStatus

FiberHandle<E, A> is the control interface for a spawned fiber:

#![allow(unused)]
fn main() {
let handle = my_effect.fork();

// Check status without blocking
let status: FiberStatus = handle.status();

// Join — blocks until the fiber completes
let exit: Exit<E, A> = handle.join().await;

// Interrupt — ask the fiber to stop
handle.interrupt();
}

FiberStatus can be Running, Completed, or Interrupted. Unlike tokio::JoinHandle, you can inspect status without consuming the handle.