Theme
Q065. What is the difference between awaiting a future and spawning a task?
Short Answer
Awaiting a future runs it as part of the current task: the parent cannot continue past that expression until the child completes, and dropping the parent also drops the child state. Spawning wraps the future in a new independently scheduled task and immediately returns a JoinHandle. Use direct awaiting for structured control flow; spawn only when work should progress independently or needs concurrency with the caller.
Deep Dive
Direct .await adds no separate task boundary. The parent future polls the child and naturally propagates borrowing, cancellation, and errors through ordinary control flow.
tokio::spawn gives the runtime an owned Send + 'static future. The caller can await its handle, abort it, or drop the handle. Dropping the handle detaches rather than cancels the task, so careless spawning can lose errors and let work outlive the request that created it.
Spawning is useful for server connections, supervised background workers, and genuinely independent operations. It is not required merely to make two futures concurrent: join! and select! can poll several futures in one task and retain structured ownership.
Internal Model
An awaited child is stored inside the parent's generated state machine. A spawned future is stored in a task allocation with its own readiness and join state and enters the runtime's scheduling queues.
The runtime guarantees that spawn does not synchronously poll the future during the call, which allows spawning while holding a lock without immediate re-entrancy. The spawned task can run soon afterward, so the lock should still be released promptly.
Example
rust
async fn compute(value: u64) -> u64 {
tokio::task::yield_now().await;
value * 2
}
#[tokio::main]
async fn main() {
let direct = compute(10).await;
let independent = tokio::spawn(compute(11));
assert_eq!(direct + independent.await.unwrap(), 42);
}Common Mistakes
- Spawning every async function to obtain concurrency.
- Dropping
JoinHandleand assuming the task is cancelled. - Ignoring task errors and panics by detaching important work.
Follow-ups
- How can
join!run futures concurrently without spawning tasks? - What ownership bounds does
tokio::spawnrequire? - How should a service supervise long-lived background tasks?
Related
- Q058. Why are Rust futures lazy?
- Q061. What is the relationship between a future, a task, and an executor?
- Q066. How does
JoinHandlebehave on completion, panic, drop, and abort?
References
- Tokio Documentation
- Async Book