电话

18600577194

当前位置: 首页 > 资讯观点 > 软件开发

学习Rust:真正实用的自定义错误类型!

标签: Rust 2026-01-21 

最近北京心玥软件公司软件开发人员一直在深入研究axum库,打算把它用在所有Rust Web项目里。Rust的Web应用框架选择不少,但看来我们都已经把Axum定为首选库了。在你继续读下去之前,如果还没了解过Axum——现在就去看看吧……我等你。

好了,回来啦!喜欢吗?必须喜欢!🥳 现在聊聊我正在做的一个学习项目。自从发现htmx后,我就沉浸在以HATEOAS为核心的Web开发世界里。用超媒体的简洁感让我特别兴奋,还帮我摆脱了JavaScript疲劳症。我建议你也去读读《超媒体系统》那本书,过去几周我一直在翻。


学习Rust

受这本书启发,我跟着示例构建了一个超媒体驱动的系统。但没像书里用Python和Flask,而是戴上我的“螃蟹帽”(Rust梗),用Rust来实现——像个大人一样。这篇帖子不细说具体怎么做(以后会补链接),重点讲怎么用Rust的牛X特性省掉一堆样板代码。

错误处理那些事儿

想当好Web开发者,就得确保返回正确的HTTP状态码(我正看着你们呢,用200 OK却带个错误消息的)。所以在我的处理器函数里,我会明确把状态码放进返回元组。比如这样:

Ok((StatusCode::OK, Html("<h1>Hello World</h1>")))  
// 或者出错时:  
Err((StatusCode::INTERNAL_SERVER_ERROR, format!("处理hello world时出错")))

这样就能告诉用户(和axum):请求要么是200 OK并附HTML,要么是500内部服务器错误加个“气人”的字符串。挺巧妙的!

有了Rust的Result枚举,我们就能处理后端抛出的各种错误。所以我会对可能失败的函数调用做匹配,根据Result返回对应内容。

实际例子:假设有个处理器函数要根据ID从数据库查联系人,代码大概长这样:

#[axum::debug_handler]  
async fn get_edit_contact(  
    State(state): State<AppState>,  
    Path(id): Path<i64>,  
) -> Result<(StatusCode, Html<String>), (StatusCode, String)> {  

    // 这个调用可能失败,先匹配Result  
    let contact = match Contact::find_by_id(&state.db, id).await {  
        // 成功:把Contact存到contact变量  
        Ok(contact) => contact,  
        // 失败:返回500状态码的元组  
        Err(e) => {  
            return Err(  
                (  
                    StatusCode::INTERNAL_SERVER_ERROR,  
                    format!("找不到联系人:{e}")  
                )  
            )  
        }  
    };  

    let edit_template = EditContactTemplate { contact };  

    // 这步也可能失败,但不用存变量,直接返回  
    match edit_template.render() {  
        // 渲染成功:返回HTML和200 OK  
        Ok(html) => Ok((StatusCode::OK, Html(html))),  
        // 又失败:还是500,继续“生气”  
        Err(e) => {  
            Err(  
                (  
                    StatusCode::INTERNAL_SERVER_ERROR,  
                    format!("模板渲染失败:{e}")  
                )  
            )  
        }  
    }  
}

这一堆样板代码可真够烦的。虽说我喜欢Rust的啰嗦劲儿(让人感觉安全又踏实),但写多了真要命——尤其当你有多个处理器函数,每个都调好几个可能失败的操作时。来,用Rust的另一个神器“新类型模式”简化它👏

造个自己的错误类型

新类型模式我不多讲,有篇超棒的指南推荐大家读。简单说,就是用轻量级包装器给现有类型(非本库原生)扩展功能。我要用它给一个叫AppError的类型实现IntoResponse trait,再让它支持把任意错误转成anyhow::Error。

先建个包装新类型:

pub struct AppError(anyhow::Error);

这里把anyhow::Error包进新类型AppError。你也可以给其他类型这么干(比如给Vec<T>包一层:struct Wrapper(Vec<T>))。接下来好玩的部分:实现trait。

要给axum的IntoResponse trait实现到这个新类型上,只需实现into_response函数,返回Response<Body>。看代码:

impl IntoResponse for AppError {  // 1. 为AppError实现IntoResponse  
    fn into_response(self) -> Response {  // 2. 唯一需要的函数,返回Response  
        (StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response()  // 3. 转成(状态码, 字符串)元组再变Response  
    }  
}

搞定!现在处理器函数就能用这种方式返回错误响应了。解释下:

  • 第1行:声明为AppError实现IntoResponse;

  • 第2行:IntoResponse唯一要求的函数,返回axum的Response;

  • 第3行:返回一个元组——状态码500 + AppError第0个元素(即anyhow::Error的字符串),再转成Response。

再来个复杂点的版本,用模板引擎返回排版好的错误页。其实就是上面的扩展版,展示它咋和现有代码配合:

impl IntoResponse for AppError {  
    fn into_response(self) -> axum::response::Response {  
        // 返回HTML格式的错误页  
        let template = Error5xxTemplate {  // 1. 用Error5xxTemplate(askama模板库的)  
            error: self.0.to_string(),  // 取AppError第0元素转字符串,适配模板  
        };  
        match template.render() {  // 2. 渲染模板  
            // 渲染成功:返回500错误页HTML  
            Ok(html) => (StatusCode::INTERNAL_SERVER_ERROR, Html(html)).into_response(),  
            // 渲染崩了:直接返回字符串  
            Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "内部服务器错误").into_response(),  
        }  
    }  
}

这里多了些细节:

  • 第1行:代码库里有用askama模板库的Error5xxTemplate结构体;

  • 第2行:确保模板渲染成功,成了就返回5xx错误页,不成就用字符串兜底。

自定义错误类型

让AppError能收各种错误

现在IntoResponse搞定了,再给AppError加个能力:能从任何地方的错误转换过来。

转换其他错误类型

为了让AppError灵活点,我想让它自动转换任意错误类型(后面会说限制)。看代码:

impl<E> From<E> for AppError  // 1. 为AppError实现From<E>  
where  
    E: Into<anyhow::Error>  // 2. 关键:E必须能转成anyhow::Error  
{  
    fn from(err: E) -> Self {  // 3. 实现from函数,把err转成Self  
        Self(err.into())  // 利用E->anyhow::Error->AppError的转换链  
    }  
}

这里用了个超好用的库anyhow,让Rust的错误处理更高效。我们借它的流行度和各库对它的转换支持。逐行解释:

  • 第1行:为AppError实现From<E>,用泛型E扩大适用范围(相当于给每个具体错误类型单独实现From);

  • 第2行:限制E必须能转成anyhow::Error,所以只支持已支持该转换的类型;

  • 第3行:实现from函数,接收错误后直接用.into()转成AppError(借助E->anyhow::Error的转换)。

用这种泛型写法,相当于自动生成了这些代码:

// SQLX错误转AppError  
impl From<sqlx::Error> for AppError {  
    fn from(err: sqlx::Error) -> Self {  
        Self(err.into())  // sqlx::Error -> anyhow::Error -> AppError  
    }  
}  
// serde_json错误转AppError  
impl From<serde_json::Error> for AppError {  
    fn from(err: serde_json::Error) -> Self {  
        Self(err.into())  // serde_json::Error -> anyhow::Error -> AppError  
    }  
}  
// reqwest错误转AppError  
impl From<reqwest::Error> for AppError {  
    fn from(err: reqwest::Error) -> Self {  
        Self(err.into())  // reqwest::Error -> anyhow::Error -> AppError  
    }  
}  
// ...其他支持转anyhow::Error的类型同理

懂了吧!

实际用起来啥样?

现在我们有了新类型AppError,也配齐了转换能力。回到之前的get_edit_contact函数,看看改完啥样:

#[axum::debug_handler]  
async fn get_edit_contact(  
    State(state): State<AppState>,  
    Path(id): Path<i64>,  
) -> Result<(StatusCode, Html<String>), AppError> {  
    // 用?自动传播错误:sqlx::Error -> AppError  
    let contact = Contact::find_by_id(&state.db, id).await?;  
    let edit_template = EditContactTemplate { contact };  
    // 用?传播错误:askama::Error -> AppError  
    let html = edit_template.render()?;  
    Ok((StatusCode::OK, Html(html)))  
}

哇,紧凑多了!这里用了?操作符——它能把错误沿调用栈向上抛。也就是说,Contact::find_by_id和.render()返回的Result里的错误,都会变成AppError新类型返回给axum。

这意味着不用在函数里手动处理错误了,直接返回统一错误类型。因为处理器函数和axum都认这个类型,皆大欢喜!🥳 好耶!


加载中~