async-task

Minimal, executor-agnostic task abstraction for spawning and running Rust futures with a single heap allocation.

Library
Cargo
v4.7.1
607stars
Apache-2.0 OR MIT

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
43/100Fair
Development Activity8
Maintenance20
Community56
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
77/100Good
Architecture90
Code Quality88
Innovation65
Learning Curve65

async-task provides the low-level Runnable/Task primitives that power the smol ecosystem’s executors (smol, async-executor, async-std) as well as several independent Rust async runtimes. Rather than shipping a full scheduler, it isolates just the part every executor needs: allocating a future on the heap alongside its waker state, tracking whether it is scheduled, running, completed, or canceled, and handing the executor a Runnable to poll plus a Task handle the caller can await.

Because the crate is no_std and requires only alloc, it works equally well on embedded targets and in full multi-threaded server executors. Any project building a custom scheduler — a thread pool, a single-threaded event loop, a WASM executor — can call spawn(), spawn_local(), or the unsafe spawn_unchecked() and receive a task shaped exactly the way this ecosystem’s other libraries expect.

What You Get

  • Runnable / Task split - spawn() returns a Runnable for the executor to poll and run, and a Task future the caller awaits for the result.
  • Single heap allocation per task - the future, its output, and its atomic state header are packed into one allocation computed via RawTask’s layout logic.
  • no_std support - the crate builds under #![no_std] with only alloc, so it runs on embedded and WASM targets, not just full std executors.
  • Builder API for metadata and panic handling - Builder::new().metadata(m) attaches arbitrary per-task metadata, and propagate_panic(true) re-raises panics through the Task future instead of the executor.
  • FallibleTask and thread-local spawning - .fallible() turns a Task into one that resolves to None instead of panicking if the Runnable is dropped unrun, and spawn_local() supports non-Send futures.

Common Use Cases

  • Building a custom async executor - implement a scheduling queue and use async_task::spawn() to convert futures into schedulable, awaitable tasks without writing waker/state-tracking code.
  • Embedding a lightweight executor in a library - crates that need their own tiny executor (a test harness, an embedded runtime) use async-task instead of pulling in a full async runtime.
  • Priority or custom scheduling - the Builder::metadata() API lets an executor attach a priority value to each task and pop them from a priority queue in order.
  • no_std / embedded async runtimes - targets like thumbv7m-none-eabi (tested in this repo’s CI) can spawn tasks with only an allocator available.

Under The Hood

Architecture async-task splits cleanly across six small modules: header.rs owns the atomic Header/HeaderWithMetadata state machine and the waker vtable, raw.rs’s RawTask computes a single packed memory layout for the header, future, and output so a task requires exactly one heap allocation, runnable.rs exposes the public spawn()/spawn_local()/spawn_unchecked() entry points plus the Runnable and Builder types, state.rs defines the SCHEDULED/RUNNING/COMPLETED/CLOSED/TASK/AWAITER bitflags that drive every transition, task.rs implements Task and FallibleTask as thin wrappers around pointer arithmetic into that header, and utils.rs supplies const-evaluable layout math and abort-on-panic guards for code that can’t unwind safely. Every state change funnels through compare-exchange loops on Header’s single AtomicUsize, so the core abstraction to protect is that bitflag state machine — changing its shape would ripple through nearly every unsafe method in runnable.rs and task.rs, which all reason about the flags directly.

Tech Stack The crate is #![no_std] with only alloc required, gated behind an optional portable-atomic dependency for targets lacking native atomics, and carries no other runtime dependencies. Its CI (via shared smol-rs GitHub Actions workflows) builds and tests across nightly, beta, and stable Rust, adds the thumbv7m-none-eabi embedded target, and layers in cargo-hack for feature-combination coverage, clippy, rustfmt, a security audit, and valgrind runs; dev-dependencies (flume, smol, futures-lite, once_cell, pin-project-lite, atomic-waker, easy-parallel) exist purely to exercise the crate’s doctests and integration tests, not to be pulled in by consumers.

Code Quality Nine integration-test files under tests/ (basic, cancel, join, metadata, panic, ready, and three waker-specific suites) exercise the task lifecycle’s edge cases, and nearly every public method carries a runnable rustdoc example enforced by RUSTDOCFLAGS=-D warnings, so the doctests double as an extensive test suite. Given how much of the crate is unsafe pointer and atomic manipulation (concentrated in header.rs, raw.rs, runnable.rs, and task.rs), each unsafe block is paired with explicit safety reasoning in comments, and CI runs clippy, rustfmt, a dependency security audit, and valgrind memory checks on every push — a level of rigor consistent with a crate other executors build on top of.

API Design The public surface is deliberately narrow: spawn() for the common Send + ‘static case, spawn_local() when std is enabled for non-Send futures on one thread, and an unsafe spawn_unchecked() escape hatch for borrowed or non-‘static futures, all funneling through the same Builder. That Builder also carries an arbitrary metadata value retrievable from both Runnable and Task — the mechanism the crate’s own doctest uses to build a priority-queue executor — and propagate_panic() lets callers choose whether a panicking task should surface through the Task future or the Runnable::run() caller. It doesn’t attempt to be a full executor or introduce new async primitives; its contribution is packaging the well-understood single-allocation, atomic-state task representation used across the smol-rs ecosystem into a reusable, no_std building block other runtimes converge on instead of reimplementing themselves.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search