Error Handling

Common Error Handling Approaches in Rust Applications

Rust's error handling differs from languages like Java—it doesn't have constructs like try...catch. The conventional approach is to define a global error handling type at the application level:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("io: `{0}`")]
    Io(#[from] io::Error),
    #[error("utf8: `{0}`")]
    FromUtf8(#[from] FromUtf8Error),
    #[error("diesel: `{0}`")]
    Diesel(#[from] diesel::result::Error),
    ...
}

pub type AppResult<T> = Result<T, AppError>;

Here, the thiserror crate is used, which conveniently helps define custom error types and simplifies code. For brevity, an AppResult type is also defined.

thiserror vs. anyhow

In Rust's error handling ecosystem, two commonly used crates are thiserror and anyhow:

  • thiserror: Suitable for library developers to define clear error types. It uses derive macros to help implement the std::error::Error trait for custom error types while allowing customization of error display. When building a library or needing to provide users with well-defined error types, thiserror is the better choice.

  • anyhow: Designed for application developers, providing a generic error type anyhow::Error that can encapsulate any error implementing the std::error::Error trait. It focuses more on error propagation rather than definition, making it ideal for application-layer code. It allows quick conversion of various errors into anyhow::Error, reducing boilerplate.

In some scenarios, you might use both crates: thiserror for defining error types in libraries and anyhow for handling and propagating those errors in applications.

Error Handling in Handlers

In Salvo, Handlers often encounter various errors, such as database connection failures, file access issues, or network errors. For these cases, the aforementioned error handling approach can be applied:

#[handler]
async fn home() -> AppResult<()> {

}

Here, home directly returns an AppResult<()>. But how should this error be displayed? We need to implement the Writer trait for the custom AppResult error type, where we can define error presentation:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.render(Text::Plain("I'm a error, hahaha!"));
    }
}

In Salvo, a Handler can return a Result as long as both the Ok and Err variants implement the Writer trait.

Error Handling with anyhow

Given the widespread use of anyhow, Salvo provides built-in support for anyhow::Error. When the anyhow feature is enabled, anyhow::Error implements the Writer trait and is mapped to InternalServerError:

#[cfg(feature = "anyhow")]
#[async_trait]
impl Writer for ::anyhow::Error {
    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
        res.render(StatusError::internal_server_error());
    }
}

To use the anyhow feature, enable it in Cargo.toml:

[dependencies]
salvo = { version = "*", features = ["anyhow"] }
anyhow = "1.0"

This allows your handler functions to directly return anyhow::Result<T>:

#[handler]
async fn home() -> anyhow::Result<impl Writer> {
    let data = fetch_data().context("Failed to fetch data")?;
    Ok(Text::Plain(data))
}

Errors often contain sensitive information that shouldn't be exposed to regular users—doing so would be insecure and a privacy breach. However, developers or administrators might prefer seeing raw error details for debugging.

Since the write method provides access to Request and Depot references, implementing conditional error display becomes straightforward:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        let user = depot.obtain::<User>();
        if user.is_admin {
            res.render(Text::Plain(e.to_string()));
        } else {
            res.render(Text::Plain("I'm a error, hahaha!"));
        }
    }
}

Displaying Error Pages

Salvo's built-in error pages suffice for most cases, rendering Html, Json, or Xml based on the request's data type. However, there may be scenarios where custom error page displays are desired.

This can be achieved by implementing a custom Catcher. For detailed instructions, refer to the Catcher section.