Rust async and Tokio
last modified November 13, 2026
In this article we write asynchronous programs in Rust with the Tokio runtime.
Asynchronous programming lets a program make progress on many tasks while some of them wait, typically for network or disk I/O. Instead of blocking a thread, a waiting task gives control back, and the thread runs another task in the meantime. Thousands of tasks can share a handful of threads.
An async fn returns a future, a value that
represents a computation that has not finished yet. The
.await operator waits for a future to complete. Futures are
lazy: they do nothing until something polls them. That something is an
executor, and the standard library does not provide one. The
most widely used async runtime is Tokio, which provides the
executor together with timers, channels, synchronization primitives, and
async file and network I/O.
The examples were tested with Rust 1.97, the 2024 edition, and Tokio
1.54.1 on a four-core machine. They are run as Cargo projects with
cargo run -q. Timings are rounded to a tenth of a second.
Setting up the project
We add Tokio with the full feature, which enables all of its
components. Production code often lists only the needed features, such
as rt-multi-thread, macros, time,
and net.
[package]
name = "tokio_example"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1.54.1", features = ["full"] }
The dependency is added with cargo add.
$ cargo add tokio --features full
Updating crates.io index
Adding tokio v1.54.1 to dependencies
Features:
+ bytes
+ fs
+ full
+ io-std
+ io-util
+ libc
+ macros
+ net
+ parking_lot
+ process
+ rt
+ rt-multi-thread
+ signal
+ signal-hook-registry
+ socket2
+ sync
+ time
+ tokio-macros
- io-uring
- mio
- schedule-latency
- taskdump
- test-util
- tracing
- windows-sys
Updating crates.io index
Locking 24 packages to latest Rust 1.97.1 compatible versions
The first async program
An async program needs a runtime to execute the futures. The
#[tokio::main] attribute creates one and runs the async
main function on it.
async fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
#[tokio::main]
async fn main() {
let fut = greet("Lucia");
println!("future created");
let msg = fut.await;
println!("{msg}");
}
The program calls an async function and awaits its result.
async fn greet(name: &str) -> String {
The function is declared to return String, but calling it
actually returns an impl Future<Output = String>.
let fut = greet("Lucia");
println!("future created");
let msg = fut.await;
Calling the function only creates the future; its body runs when the
future is awaited. .await can only be used inside async
functions and blocks.
#[tokio::main]
async fn main() {
The macro turns the async main into a normal one that builds
a multi-threaded runtime and calls block_on on the body.
$ cargo run -q future created Hello, Lucia!
Futures are lazy
A future that is never awaited never runs.
async fn work() {
println!("working");
}
#[tokio::main]
async fn main() {
work();
println!("done");
}
We call an async function without .await.
work();
The future is created and immediately dropped. The compiler warns about
it, and the message working is never printed. This differs
from JavaScript promises and C# tasks, which start running when they are
created.
$ cargo run -q warning: unused implementer of `Future` that must be used --> src/main.rs:7:5 | 7 | work(); | ^^^^^^ | = note: futures do nothing unless you `.await` or poll them = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default done
Sequential awaits
Awaiting futures one after another runs them one after another.
use std::time::{Duration, Instant};
use tokio::time::sleep;
async fn fetch(name: &str, ms: u64) -> String {
sleep(Duration::from_millis(ms)).await;
format!("{name} ({ms} ms)")
}
#[tokio::main]
async fn main() {
let start = Instant::now();
let a = fetch("users", 300).await;
let b = fetch("orders", 500).await;
let c = fetch("stats", 200).await;
println!("{a}, {b}, {c}");
println!("elapsed: {:.1}s", start.elapsed().as_secs_f64());
}
The fetch function simulates a network request with an
async sleep.
sleep(Duration::from_millis(ms)).await;
The tokio::time::sleep function returns a future that
completes after the given time. While it waits, the thread is free to
run other tasks.
let a = fetch("users", 300).await;
let b = fetch("orders", 500).await;
let c = fetch("stats", 200).await;
Each request starts only after the previous one has finished, so the total time is the sum of the three delays.
$ cargo run -q users (300 ms), orders (500 ms), stats (200 ms) elapsed: 1.0s
Concurrency with join!
The tokio::join! macro polls several futures concurrently
and waits until all of them complete.
use std::time::{Duration, Instant};
use tokio::time::sleep;
async fn fetch(name: &str, ms: u64) -> String {
sleep(Duration::from_millis(ms)).await;
println!("{name} ready");
format!("{name} ({ms} ms)")
}
async fn parse(text: &str) -> Result<i32, String> {
sleep(Duration::from_millis(100)).await;
text.parse().map_err(|_| format!("bad number: {text}"))
}
#[tokio::main]
async fn main() {
let start = Instant::now();
let (a, b, c) = tokio::join!(fetch("users", 300), fetch("orders", 500), fetch("stats", 200));
println!("{a}, {b}, {c}");
println!("elapsed: {:.1}s", start.elapsed().as_secs_f64());
let res = tokio::try_join!(parse("12"), parse("x7"), parse("30"));
println!("{res:?}");
}
The same three requests now overlap.
let (a, b, c) = tokio::join!(fetch("users", 300), fetch("orders", 500), fetch("stats", 200));
The total time equals the longest delay. The futures complete in the
order of their delays, but the results are returned as a tuple in the
order of the arguments. All three futures run within the current task;
join! gives concurrency, not parallelism.
let res = tokio::try_join!(parse("12"), parse("x7"), parse("30"));
The try_join! macro works with futures returning
Result. It returns the first error and drops the remaining
futures, or a tuple of all Ok values.
$ cargo run -q
stats ready
users ready
orders ready
users (300 ms), orders (500 ms), stats (200 ms)
elapsed: 0.5s
Err("bad number: x7")
Spawning tasks
The tokio::spawn function submits a future to the runtime as
an independent task. Tasks are the async counterpart of
threads, but much lighter. On the multi-threaded runtime, they run in
parallel on a pool of worker threads.
use std::time::Duration;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
let mut handles = Vec::new();
for id in 1..=4u64 {
let handle = tokio::spawn(async move {
sleep(Duration::from_millis(400 - id * 100)).await;
println!("task {id} finished");
id * id
});
handles.push(handle);
}
let mut results = Vec::new();
for h in handles {
results.push(h.await.unwrap());
}
println!("results: {results:?}");
}
We spawn four tasks that finish in reverse order.
let handle = tokio::spawn(async move {
sleep(Duration::from_millis(400 - id * 100)).await;
println!("task {id} finished");
id * id
});
A spawned task may outlive the function that spawned it, so the future
must be 'static: it cannot borrow local variables. The
move keyword moves id into the async block. A
task starts running immediately, even if its handle is never awaited.
results.push(h.await.unwrap());
Awaiting a JoinHandle returns
Result<T, JoinError>, which is an error when the task
panicked or was cancelled.
$ cargo run -q task 4 finished task 3 finished task 2 finished task 1 finished results: [1, 4, 9, 16]
Collecting results with JoinSet
A JoinSet holds a group of tasks and returns their results
in the order they complete.
use std::time::{Duration, Instant};
use tokio::task::JoinSet;
use tokio::time::sleep;
async fn download(url: String, ms: u64) -> (String, usize) {
sleep(Duration::from_millis(ms)).await;
let len = url.len() * 100;
(url, len)
}
#[tokio::main]
async fn main() {
let jobs = [("example.com/a", 500), ("example.com/bb", 100), ("example.com/ccc", 300)];
let start = Instant::now();
let mut set = JoinSet::new();
for (url, ms) in jobs {
set.spawn(download(url.to_string(), ms));
}
let mut total = 0;
while let Some(res) = set.join_next().await {
let (url, len) = res.unwrap();
println!("{:.1}s {url}: {len} bytes", start.elapsed().as_secs_f64());
total += len;
}
println!("total: {total} bytes");
}
We simulate three downloads of different durations.
set.spawn(download(url.to_string(), ms));
The spawn method of the set spawns a task and keeps its
handle.
while let Some(res) = set.join_next().await {
The join_next method waits for the next task to finish and
returns None when the set is empty. Results can be
processed as soon as they are ready. When a JoinSet is
dropped, all tasks still in it are aborted.
$ cargo run -q 0.1s example.com/bb: 1400 bytes 0.3s example.com/ccc: 1500 bytes 0.5s example.com/a: 1300 bytes total: 4200 bytes
Racing futures with select!
The tokio::select! macro waits on several futures and runs
the branch of the first one to complete. The other futures are dropped,
which cancels them.
use std::time::Duration;
use tokio::time::sleep;
async fn mirror(name: &str, ms: u64) -> String {
sleep(Duration::from_millis(ms)).await;
format!("data from {name}")
}
#[tokio::main]
async fn main() {
let winner = tokio::select! {
res = mirror("europe", 300) => res,
res = mirror("america", 150) => res,
res = mirror("asia", 400) => res,
};
println!("{winner}");
let res = tokio::select! {
data = mirror("slow", 2000) => Some(data),
_ = sleep(Duration::from_millis(500)) => None,
};
println!("{res:?}");
}
We ask three mirrors for the same data and use the fastest answer.
let winner = tokio::select! {
res = mirror("europe", 300) => res,
res = mirror("america", 150) => res,
res = mirror("asia", 400) => res,
};
Each branch has a pattern, a future, and a handler. The value of the
whole select! is the value of the chosen handler.
data = mirror("slow", 2000) => Some(data),
_ = sleep(Duration::from_millis(500)) => None,
Racing a future against a sleep implements a timeout. The slow request is cancelled after half a second.
$ cargo run -q data from america None
Timeouts and cancellation
The tokio::time::timeout function wraps a future with a time
limit.
use std::time::Duration;
use tokio::time::{sleep, timeout};
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) {
println!("{} dropped", self.0);
}
}
async fn query(name: &'static str, ms: u64) -> String {
let _g = Guard(name);
sleep(Duration::from_millis(ms)).await;
println!("{name} completed");
format!("{name} result")
}
#[tokio::main]
async fn main() {
let limit = Duration::from_millis(250);
match timeout(limit, query("fast", 100)).await {
Ok(v) => println!("ok: {v}"),
Err(e) => println!("error: {e}"),
}
match timeout(limit, query("slow", 1000)).await {
Ok(v) => println!("ok: {v}"),
Err(e) => println!("error: {e}"),
}
}
A Guard value prints a message when it is dropped, so we can
see what happens to a cancelled future.
match timeout(limit, query("slow", 1000)).await {
Ok(v) => println!("ok: {v}"),
Err(e) => println!("error: {e}"),
}
The function returns Ok with the output when the future
finishes in time, or an Elapsed error.
When the time is up, the slow future is dropped while waiting at its
.await. Its local variables are dropped, so the guard
message appears, but the code after the .await never runs:
slow completed is not printed. In async Rust, any
.await is a point where the function can be cancelled.
$ cargo run -q fast completed fast dropped ok: fast result slow dropped error: deadline has elapsed
Channels
Tasks communicate through channels. The tokio::sync::mpsc
channel has multiple producers and a single consumer.
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<String>(8);
for id in 1..=3 {
let tx = tx.clone();
tokio::spawn(async move {
for n in 1..=2 {
sleep(Duration::from_millis(id * 70)).await;
tx.send(format!("sensor {id}: reading {n}")).await.unwrap();
}
});
}
drop(tx);
let mut count = 0;
while let Some(msg) = rx.recv().await {
println!("{msg}");
count += 1;
}
println!("received {count} messages");
}
Three sensor tasks send readings to the main task.
let (tx, mut rx) = mpsc::channel::<String>(8);
The channel is bounded and holds at most eight messages. When it is full,
send waits until there is room, which slows down fast
producers. This is called back-pressure.
let tx = tx.clone();
tokio::spawn(async move {
Each producer task gets its own clone of the sender.
drop(tx);
while let Some(msg) = rx.recv().await {
The recv method returns None when all senders
are gone. We drop the original sender, so the loop ends after the last
producer finishes.
$ cargo run -q sensor 1: reading 1 sensor 2: reading 1 sensor 1: reading 2 sensor 3: reading 1 sensor 2: reading 2 sensor 3: reading 2 received 6 messages
Request and response with oneshot
A oneshot channel sends a single value. It is a common way
to get a reply from another task.
use tokio::sync::{mpsc, oneshot};
enum Command {
Add(i64),
Get(oneshot::Sender<i64>),
}
async fn counter(mut rx: mpsc::Receiver<Command>) {
let mut total = 0;
while let Some(cmd) = rx.recv().await {
match cmd {
Command::Add(n) => total += n,
Command::Get(reply) => {
let _ = reply.send(total);
}
}
}
println!("counter stopped");
}
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(16);
let actor = tokio::spawn(counter(rx));
for n in [5, 10, -3] {
tx.send(Command::Add(n)).await.unwrap();
}
let (reply_tx, reply_rx) = oneshot::channel();
tx.send(Command::Get(reply_tx)).await.unwrap();
println!("total: {}", reply_rx.await.unwrap());
drop(tx);
actor.await.unwrap();
}
A counter task owns its state and handles commands from a channel. This pattern is known as an actor.
enum Command {
Add(i64),
Get(oneshot::Sender<i64>),
}
The Get command carries the sender half of a oneshot
channel, through which the actor sends the answer.
let (reply_tx, reply_rx) = oneshot::channel();
tx.send(Command::Get(reply_tx)).await.unwrap();
println!("total: {}", reply_rx.await.unwrap());
The receiver half is itself a future. Because the counter state is owned by a single task, it needs no lock.
$ cargo run -q total: 12 counter stopped
Shared state
State shared between tasks is wrapped in Arc and a mutex,
as with threads.
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
let cache: Arc<Mutex<HashMap<String, u32>>> = Arc::new(Mutex::new(HashMap::new()));
let mut handles = Vec::new();
for word in ["apple", "pear", "apple", "plum", "apple", "pear"] {
let cache = Arc::clone(&cache);
handles.push(tokio::spawn(async move {
let mut map = cache.lock().await;
sleep(Duration::from_millis(10)).await;
*map.entry(word.to_string()).or_insert(0) += 1;
}));
}
for h in handles {
h.await.unwrap();
}
let map = cache.lock().await;
let mut entries: Vec<_> = map.iter().collect();
entries.sort();
println!("{entries:?}");
}
Several tasks count words in a shared map.
let mut map = cache.lock().await; sleep(Duration::from_millis(10)).await;
The tokio::sync::Mutex is locked with .await,
so a task waiting for the lock does not block the thread. Its guard can
be held across an .await, as here.
The std::sync::Mutex is faster and is the better choice
when the lock is held only briefly and never across an
.await. Holding a standard guard across an
.await makes the future not Send, and
tokio::spawn rejects it.
$ cargo run -q
[("apple", 3), ("pear", 2), ("plum", 1)]
Do not block the runtime
Async tasks share threads, so a task that blocks the thread, by sleeping synchronously or computing for a long time, stops all other tasks on that thread.
use std::time::{Duration, Instant};
async fn tick(name: &str, start: Instant) {
for _ in 0..3 {
tokio::time::sleep(Duration::from_millis(100)).await;
println!("{name} tick at {:.1}s", start.elapsed().as_secs_f64());
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let start = Instant::now();
let bad = async {
std::thread::sleep(Duration::from_millis(500));
println!("blocking work done at {:.1}s", start.elapsed().as_secs_f64());
};
tokio::join!(tick("A", start), bad);
println!("---");
let start = Instant::now();
let good = async {
tokio::task::spawn_blocking(|| std::thread::sleep(Duration::from_millis(500)))
.await
.unwrap();
println!("blocking work done at {:.1}s", start.elapsed().as_secs_f64());
};
tokio::join!(tick("B", start), good);
}
We use the single-threaded runtime to make the effect clearly visible.
#[tokio::main(flavor = "current_thread")]
The current_thread flavor runs all tasks on the main thread.
let bad = async {
std::thread::sleep(Duration::from_millis(500));
The standard thread::sleep blocks the thread. The ticker
cannot run for half a second; its first tick comes at 0.5s instead of
0.1s.
tokio::task::spawn_blocking(|| std::thread::sleep(Duration::from_millis(500)))
.await
.unwrap();
The spawn_blocking function runs blocking code on a separate
thread pool and returns a future of its result. The ticker keeps its
schedule. Use it for blocking libraries, synchronous file access, and
CPU-heavy computations.
$ cargo run -q blocking work done at 0.5s A tick at 0.5s A tick at 0.6s A tick at 0.7s --- B tick at 0.1s B tick at 0.2s B tick at 0.3s blocking work done at 0.5s
Async file I/O
The tokio::fs module mirrors std::fs with async
functions.
use tokio::fs;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[tokio::main]
async fn main() -> std::io::Result<()> {
fs::write("words.txt", "falcon\nsky\ncloud\nrock\n").await?;
let text = fs::read_to_string("words.txt").await?;
println!("{} bytes, {} lines", text.len(), text.lines().count());
let mut file = fs::OpenOptions::new().append(true).open("words.txt").await?;
file.write_all(b"forest\n").await?;
file.flush().await?;
let file = fs::File::open("words.txt").await?;
let mut lines = BufReader::new(file).lines();
while let Some(line) = lines.next_line().await? {
print!("{} ", line.to_uppercase());
}
println!();
fs::remove_file("words.txt").await?;
Ok(())
}
We write a file, append to it, and read it line by line.
fs::write("words.txt", "falcon\nsky\ncloud\nrock\n").await?;
The async versions have the same names and arguments as their standard counterparts.
let mut lines = BufReader::new(file).lines();
while let Some(line) = lines.next_line().await? {
The AsyncBufReadExt trait provides lines, and
next_line returns Ok(None) at the end of the
file. Operating systems offer no portable async file API, so Tokio runs
file operations with spawn_blocking internally.
$ cargo run -q 22 bytes, 4 lines FALCON SKY CLOUD ROCK FOREST
A TCP echo server
Network I/O is where async Rust shines. The program starts a server that returns each line in uppercase, and connects two clients to it.
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
async fn handle(stream: TcpStream) -> std::io::Result<()> {
let (reader, mut writer) = stream.into_split();
let mut lines = BufReader::new(reader).lines();
while let Some(line) = lines.next_line().await? {
writer.write_all(format!("{}\n", line.to_uppercase()).as_bytes()).await?;
}
Ok(())
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
tokio::spawn(async move {
loop {
let (stream, peer) = listener.accept().await.unwrap();
println!("server: connection from {}", peer.ip());
tokio::spawn(handle(stream));
}
});
let mut clients = Vec::new();
for name in ["anna", "peter"] {
clients.push(tokio::spawn(async move {
let stream = TcpStream::connect(addr).await?;
let (reader, mut writer) = stream.into_split();
writer.write_all(format!("hello from {name}\n").as_bytes()).await?;
writer.shutdown().await?;
let mut lines = BufReader::new(reader).lines();
let reply = lines.next_line().await?;
Ok::<_, std::io::Error>(reply)
}));
}
for c in clients {
println!("client got {:?}", c.await.unwrap()?);
}
Ok(())
}
Server and clients run in one process as separate tasks.
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
Port 0 asks the operating system for any free port. We read the actual address back for the clients.
loop {
let (stream, peer) = listener.accept().await.unwrap();
println!("server: connection from {}", peer.ip());
tokio::spawn(handle(stream));
}
The accept loop spawns a task for each connection, so the server handles clients concurrently. A thread per connection would be far more expensive with many clients.
writer.write_all(format!("hello from {name}\n").as_bytes()).await?;
writer.shutdown().await?;
The client splits the stream into a read half and a write half. Shutting down the write half tells the server that no more lines are coming.
$ cargo run -q
server: connection from 127.0.0.1
server: connection from 127.0.0.1
client got Some("HELLO FROM ANNA")
client got Some("HELLO FROM PETER")
Periodic tasks with interval
The tokio::time::interval function creates a ticker that
fires at a fixed period.
use std::time::{Duration, Instant};
use tokio::time::{MissedTickBehavior, interval};
#[tokio::main]
async fn main() {
let start = Instant::now();
let mut ticker = interval(Duration::from_millis(200));
for i in 1..=4 {
ticker.tick().await;
println!("tick {i} at {:.1}s", start.elapsed().as_secs_f64());
}
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
tokio::time::sleep(Duration::from_millis(500)).await;
ticker.tick().await;
println!("late tick at {:.1}s", start.elapsed().as_secs_f64());
ticker.tick().await;
println!("next tick at {:.1}s", start.elapsed().as_secs_f64());
}
We wait for several ticks, then fall behind on purpose.
ticker.tick().await;
The first tick completes immediately. Unlike sleeping in a loop, the ticker keeps a fixed schedule, so the time spent between ticks does not accumulate as drift.
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
After the 500 ms pause, two ticks were missed. With the
Skip behavior, the late tick fires at once and the next
one returns to the original 200 ms grid. The default Burst
behavior would fire the missed ticks in quick succession.
$ cargo run -q tick 1 at 0.0s tick 2 at 0.2s tick 3 at 0.4s tick 4 at 0.6s late tick at 1.1s next tick at 1.2s
Limiting concurrency
Starting too many operations at once can overload a server or exhaust
file handles. A Semaphore limits how many tasks do the work
at the same time.
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
let limit = Arc::new(Semaphore::new(2));
let start = Instant::now();
let mut set = JoinSet::new();
for id in 1..=6 {
let limit = Arc::clone(&limit);
set.spawn(async move {
let _permit = limit.acquire().await.unwrap();
println!("job {id} starts at {:.1}s", start.elapsed().as_secs_f64());
sleep(Duration::from_millis(300)).await;
});
}
set.join_all().await;
println!("all done at {:.1}s", start.elapsed().as_secs_f64());
}
Six jobs run, at most two at a time.
let _permit = limit.acquire().await.unwrap();
The semaphore has two permits. A task that cannot get one waits. The
permit is returned when _permit is dropped at the end of
the task. Note that the name must not be just _, which
would drop the permit immediately.
set.join_all().await;
The join_all method waits for all tasks in the set. Six
jobs of 300 ms in pairs take 0.9 seconds.
$ cargo run -q job 1 starts at 0.0s job 2 starts at 0.0s job 3 starts at 0.3s job 4 starts at 0.3s job 6 starts at 0.6s job 5 starts at 0.6s all done at 0.9s
Errors, panics, and cancellation
A task can return an error, panic, or be aborted. The
JoinHandle reports which one happened.
use std::time::Duration;
async fn risky(n: u32) -> Result<u32, String> {
tokio::time::sleep(Duration::from_millis(10 * n as u64)).await;
if n == 3 {
return Err(format!("job {n} failed"));
}
if n == 4 {
panic!("job {n} panicked");
}
Ok(n * 10)
}
#[tokio::main]
async fn main() {
let handles: Vec<_> = (1..=4).map(|n| tokio::spawn(risky(n))).collect();
for (i, h) in handles.into_iter().enumerate() {
match h.await {
Ok(Ok(v)) => println!("job {}: {v}", i + 1),
Ok(Err(e)) => println!("job {}: error: {e}", i + 1),
Err(e) if e.is_panic() => println!("job {}: panicked", i + 1),
Err(e) => println!("job {}: {e}", i + 1),
}
}
let task = tokio::spawn(async {
tokio::time::sleep(Duration::from_secs(10)).await;
});
task.abort();
let err = task.await.unwrap_err();
println!("cancelled: {}", err.is_cancelled());
}
The fourth job panics; the third returns an error.
Ok(Ok(v)) => println!("job {}: {v}", i + 1),
Ok(Err(e)) => println!("job {}: error: {e}", i + 1),
Err(e) if e.is_panic() => println!("job {}: panicked", i + 1),
The outer Result comes from the task, the inner one from our
function. A panic in a spawned task does not crash the program; the
runtime catches it, prints the message, and reports it through the
handle.
task.abort(); let err = task.await.unwrap_err();
The abort method cancels a task at its next
.await. Awaiting the handle then returns a cancelled
JoinError.
$ cargo run -q job 1: 10 job 2: 20 job 3: error: job 3 failed thread 'tokio-rt-worker' (167716) panicked at src/main.rs:9:9: job 4 panicked note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace job 4: panicked cancelled: true
Source
Asynchronous Programming in Rust
In this article we have written asynchronous programs with Tokio. Thread-based concurrency is covered in Rust threads and Rust channels.
Author
List all Rust tutorials.