首页 / Rust 入门教程 / async 中的错误处理

Rust 入门教程

async 中的错误处理

本教程共 78 篇 · 第 73 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

RustRust 入门教程异步错误处理Resultasyncanyhowthiserror

本节目标:掌握异步函数如何返回 Result、在 async 里用 ? 配合 .await 串起错误传播,理解 tokio::spawn 对错误类型的约束,并了解用 anyhow/thiserror 给异步代码减负。

Rust 的错误处理靠 Result<T, E>(第 9 章讲过)。到了异步世界,这套机制基本不变,只是多了一层 .await。这一章把异步错误处理讲清楚,免得你在 async fn 里被 ?.await 的顺序搞晕。

1-1 异步函数照样返回 Result

一个异步函数完全可以像普通函数一样返回 Result。区别只是它前面多了 async,返回类型是 Future<Output = Result<T, E>>

use std::io;

async fn read_config() -> Result<String, io::Error> {
    // 假设这是一个异步读文件的操作
    tokio::fs::read_to_string("config.toml").await
}

注意这里 .await 出现在 read_to_string(...) 后面:先 .await 拿到 Result,函数再把这个 Result 返回出去。调用方拿到的是 Result<String, io::Error>,该怎么处理还怎么处理。

Note

tokio::fs 提供了和标准库 std::fs 对应的异步版本。凡是异步 IO,都要用 tokio 提供的异步 API,并在末尾 .await,否则它不会真正执行。

1-2 ? 与 .await 的顺序

async fn 内部传播错误,你既要 .await 拿到 Future 的结果,又要对结果用 ? 提前返回错误。顺序是:.await,再 ?

use std::io;

async fn read_config() -> Result<String, io::Error> {
    tokio::fs::read_to_string("config.toml").await
}

async fn use_config() -> Result<(), io::Error> {
    let content = read_config().await?; // 先 await 拿到 Result,再 ? 传播
    println!("配置长度: {}", content.len());
    Ok(())
}

read_config().await? 的含义是:等 read_config 这个 Future 完成,得到 Result;如果是 Err,就立刻从 use_config 返回这个错误;如果是 Ok,把里面的 String 赋给 content。和普通同步代码里 ? 的用法一模一样,只是中间插了个 .await

一个常见的初学者错误是写成 read_config()?.await——这会先对”还没执行的 Future”用 ?,而 Future 本身不是 Result,直接编译报错。记住顺序:.await 在前,? 在后。

1-3 tokio::spawn 对错误类型的约束

tokio::spawn 派发的任务,其 Future 必须满足 'static,而且执行器在某些配置下对返回的错误类型也有期望。最直接的做法是:让 spawn 的任务自己处理错误,或者返回能被 JoinHandle 接住的 Result

use std::io;

async fn may_fail() -> Result<(), io::Error> {
    tokio::fs::read_to_string("missing.txt").await?;
    Ok(())
}

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(may_fail());

    match handle.await {
        Ok(Ok(())) => println!("任务成功"),
        Ok(Err(e)) => println!("任务内部出错: {}", e),
        Err(e) => println!("任务 panic 了: {}", e), // JoinError
    }
}

这里外层 Ok(Err(e)) 是”任务正常结束但返回了错误”,内层 Err(e) 是任务里 may_failio::Error;而最外层的 Err(e)JoinError,表示任务自身 panic 了或被取消了。分清这两层,异步错误才好处理。

Warning

tokio::spawn 里的闭包/函数如果借用外部变量,会因为不满足 'static 报错。需要传数据,用 move 把所有权搬进去,或 Arc::clone 共享。这个规则和第 65 章的线程闭包一致。

1-4 用 anyhow 给异步代码减负

写业务时,每个函数都去精确定义自己的错误类型会很累。社区里 anyhow 提供了一个”兜底”错误类型 anyhow::Error,能装下任何实现了 std::error::Error 的错误,用 ? 自动转换:

use anyhow::Result;

async fn read_config() -> Result<String> {
    let s = tokio::fs::read_to_string("config.toml").await?; // io::Error 自动转成 anyhow::Error
    Ok(s)
}

#[tokio::main]
async fn main() -> Result<()> {
    let content = read_config().await?;
    println!("读到 {} 字节", content.len());
    Ok(())
}

在异步函数里用 anyhow 和同步里完全一样:函数返回 anyhow::Result<T>(即 Result<T, anyhow::Error>),内部 ? 自动把各种具体错误装箱成 anyhow::Error。它最适合应用层、main 函数这类”不想精细化定义错误”的地方。截至核对时,anyhow 的最新稳定版是 1.0.x 系列(约 1.0.104)

1-5 用 thiserror 定义库的错误类型

如果你在写一个给别人用的库,应该定义自己的、精确的错误枚举,方便调用方匹配处理。这时用 thiserror#[derive(Error)] 最省力:

use thiserror::Error;

#[derive(Error, Debug)]
enum MyError {
    #[error("配置文件缺失: {0}")]
    Missing(String),
    #[error("IO 错误")]
    Io(#[from] std::io::Error),
}

async fn read_config(path: &str) -> Result<String, MyError> {
    let s = tokio::fs::read_to_string(path).await?; // io::Error 经 #[from] 转成 MyError::Io
    if s.is_empty() {
        return Err(MyError::Missing(path.to_string()));
    }
    Ok(s)
}

#[from]? 能自动把 io::Error 转成 MyError::Io,异步同步通用。截至核对时,thiserror 的最新稳定版是 2.0.x 系列(约 2.0.18),2.x 相比 1.x 主要是改进了衍生实现,对异步代码没有特殊差异。

Tip

经验搭配:库用 thiserror 定义精准错误,应用层用 anyhow 兜底。两者可以共存——库返回 thiserror 的错,应用层用 anyhow? 自动接住。

1-6 异步与同步错误混用

实际项目里异步和同步代码经常交织。要点:同步函数返回 Result,在异步函数里照样能用 ?(但它不会 .await,因为本来就不是 Future);反过来,异步函数不能直接在同步函数里调用——你要么在异步上下文里 .await,要么用运行时的 block_on 强转(但 block_on 在异步任务里用会出问题,慎用)。

1-7 一个综合示例:从库错误到应用兜底

把前面几点串起来,看一个完整的小场景:一个库函数用 thiserror 定义精准错误,调用方在异步 main 里用 anyhow 兜底,中间用 ? 自动衔接。

use anyhow::Result;
use thiserror::Error;

#[derive(Error, Debug)]
enum ConfigError {
    #[error("读取失败")]
    Io(#[from] std::io::Error),
}

async fn load() -> Result<String, ConfigError> {
    let s = tokio::fs::read_to_string("a.txt").await?;
    Ok(s)
}

#[tokio::main]
async fn main() -> Result<()> {
    let _ = load().await?;
    Ok(())
}

这里 load 返回库自定义的 ConfigErrormain 返回 anyhow::Result<()>。中间的 ? 能把 ConfigError 自动装箱进 anyhow::Error,全程不需要手写转换函数。这种”库精准、应用兜底”的分工,是 Rust 错误处理的事实标准,异步同步都适用。

如果某个错误你确实没法恢复,也可以直接 return Err(e.into()),或用 anyhow::bail!("出错啦: {}", x) 提前返回。错误处理没有唯一正确答案,关键是保持一致性:要么全程 thiserror 精确化,要么在边界用 anyhow 兜底,别在一个项目里换来换去让后人看不懂。

Note

新手常纠结”我的错误类型到底该怎么设计”。建议:写库就 thiserror 定义枚举,写应用就 anyhow 兜底,二者通过 #[from] 无缝衔接。这套组合能覆盖九成场景,等真遇到特殊需求再深入。

1-8 错误处理的性能与权衡

从性能角度,返回 Result 几乎没有运行时开销——错误就是个普通的枚举值,没有异常那种栈展开成本。这也解释了为什么 Rust 鼓励用 Result 而非 panic 来处理可预期的错误。anyhow 因为要把错误装进 trait 对象,会有一次小的堆分配;在极端热路径上,如果 profiling 显示它成了瓶颈,可以退回具体错误类型。但绝大多数业务代码,这点开销可以忽略不计。

Note

panic 用于”真的不该发生”的程序员错误(如数组越界),不是常规错误处理手段。异步任务里 panic 会被 JoinHandle 捕获成 JoinError,不会炸掉整个进程,但也不该依赖它来传递业务错误。业务错误,永远走 Result。把错误处理想清楚,异步代码就能既安全又易读,不会因为一个未处理的错误类型而编译不过,也不会在运行时悄悄 panic。

1-9 小结

总之,异步错误处理 = 同步 Result/? 的那一套 + 牢记”.await 在前、? 在后”。工具上,anyhow 管应用、thiserror 管库,错误就不再是你写异步的负担。当你能把 Result.await? 三者在异步函数里流畅地串起来,错误处理就不再是心智负担,反而成了代码健壮性的保障。下一章我们换话题,聊 Rust 里另一个强大又容易被误解的特性——宏。