Error Handling

Common Error Handling Approaches in Rust Applications

Error handling in Rust differs from languages like Java; it lacks constructs like try...catch. The typical 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 library is used, which facilitates defining custom error types and simplifies code. For brevity, an AppResult type alias is also defined.

thiserror vs. anyhow

In the Rust error handling ecosystem, two commonly used libraries 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 you to define error representations. When building a library or providing clear error types to users, thiserror is the better choice.

  • anyhow: Geared toward application developers, it provides a generic error type anyhow::Error that can encapsulate any error implementing the std::error::Error trait. It focuses more on error propagation than definition, making it particularly suitable for application-layer code. You can quickly convert various errors into anyhow::Error, reducing the need for boilerplate code.

In some scenarios, you might use both libraries: define error types with thiserror in libraries and handle and propagate these errors with anyhow in applications.

Error Handling in Handlers

In Salvo, Handlers often encounter various errors, such as database connection errors, file access errors, network connection errors, etc. For these types of errors, 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 error type AppResult, where we can decide how to display the error:

#[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, provided that both the Ok and Err types in the Result 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 Salvo's anyhow feature 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, which generally shouldn't be visible to regular users for security and privacy reasons. However, if you're a developer or site administrator, you might prefer errors to be fully exposed, revealing the most accurate error information.

As shown, in the write method, we can access references to Request and Depot, making it convenient to implement the above approach:

#[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 meet requirements in most cases, displaying Html, Json, or Xml pages based on the request's data type. However, there are situations where custom error page displays are still desired.

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