Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Async API Design and Task Lifecycle

Rule

Keep async signatures and spawned-task ownership explicit; reusable libraries expose awaitable work or return an owner instead of hiding tasks.

Why

Unowned tasks detach, hide failures, and make teardown and tests unreliable.

Activation

Apply this page for async APIs, async traits, Send + 'static bounds, spawning, task owners, or join handles.

Do

  • Use async fn -> Result<T, E> for one-shot work callers should await.
  • Introduce async traits only when callers need an abstraction.
  • Add Send + 'static only where values cross a spawned or stored-future boundary.
  • Spawn from an owner that stores handles and task-specific state.
  • Use responsibility names such as Poller, WorkerSet, or Supervisor.
  • Store JoinHandle<Result<(), Error>> when failures must be reported.
  • Return an owner or guard when a library truly owns background work.
  • Follow cancellation and shutdown for signals, joining, and teardown.

Avoid

  • Do not drop a handle for important work.
  • Do not assume dropping JoinHandle cancels its task.
  • Do not spawn inside a constructor unless the returned value owns the lifecycle.
  • Do not swallow task errors.
  • Do not spawn merely to make a library API appear nonblocking.
  • Do not add Send, Sync, or 'static to ordinary async APIs by habit.

Library vs Application

Applications own spawning and task groups. Libraries normally expose futures or streams and let callers choose where to spawn.

Example

#![allow(unused)]
fn main() {
#[must_use = "call `shutdown` to stop and join the task"]
pub struct Poller {
    shutdown: CancellationToken,
    task: JoinHandle<Result<(), PollerError>>,
}

impl Poller {
    pub async fn shutdown(self) -> Result<(), PollerError> {
        self.shutdown.cancel();
        self.task.await.map_err(PollerError::Join)?
    }
}
}

The attribute catches a discarded return value; the explicit method owns actual cancellation and joining.

Exceptions

  • Fire-and-forget only disposable, documented best-effort work.
  • Tests may spawn short tasks when they abort or join them.
  • Convenience APIs may spawn when they return lifecycle control.