How to organise application Error in actix-web application
Into
In this article, we will learn how to organise application Error in actix-web application.
Every backend application has a dao layer(or data access object) and a http layer.
For dao layer, usually we use lib::Result
as return type. Below is an example of fetching all users when using clickhosue-rs
crate:
#![allow(unused)] fn main() { use clickhouse::error::{Error, Result}; pub async fn get_all_users(client: &Client) -> Result<Vec<User>> { let users = client .query("SELECT ?fields FROM users") .fetch_all::<User>() .await?; Ok(users) } }
Notice, the Result
is not std::result::Result
type, it is a type that use clickhouse::error::Error
as the error type in Result
generic type.
#![allow(unused)] fn main() { use std::{error::Error as StdError, fmt, io, result, str::Utf8Error}; use serde::{de, ser}; /// A result with a specified [`Error`] type. pub type Result<T, E = Error> = result::Result<T, E>; /// Represents all possible errors. #[derive(Debug, thiserror::Error)] #[non_exhaustive] #[allow(missing_docs)] pub enum Error { #[error("invalid params: {0}")] InvalidParams(#[source] Box<dyn StdError + Send + Sync>), #[error("network error: {0}")] Network(#[source] Box<dyn StdError + Send + Sync>), #[error("compression error: {0}")] Compression(#[source] Box<dyn StdError + Send + Sync>), #[error("decompression error: {0}")] Decompression(#[source] Box<dyn StdError + Send + Sync>), #[error("no rows returned by a query that expected to return at least one row")] RowNotFound, #[error("sequences must have a known size ahead of time")] SequenceMustHaveLength, #[error("`deserialize_any` is not supported")] DeserializeAnyNotSupported, #[error("not enough data, probably a row type mismatches a database schema")] NotEnoughData, #[error("string is not valid utf8")] InvalidUtf8Encoding(#[from] Utf8Error), #[error("tag for enum is not valid")] InvalidTagEncoding(usize), #[error("a custom error message from serde: {0}")] Custom(String), #[error("bad response: {0}")] BadResponse(String), #[error("timeout expired")] TimedOut, // Internally handled errors, not part of public API. // XXX: move to another error? #[error("internal error: too small buffer, need another {0} bytes")] #[doc(hidden)] TooSmallBuffer(usize), } }
Actix-web http layer
Actix-web framework requires the error type is actix_web::error::Error
if actix_web::Result
is used as the return type.
Result
type in actix-web :
#![allow(unused)] fn main() { pub use self::error::Error; pub use self::internal::*; pub use self::response_error::ResponseError; pub(crate) use macros::{downcast_dyn, downcast_get_type_id}; /// A convenience [`Result`](std::result::Result) for Actix Web operations. /// /// This type alias is generally used to avoid writing out `actix_http::Error` directly. pub type Result<T, E = Error> = std::result::Result<T, E>; }
Error
type in actix-web :
#![allow(unused)] fn main() { /// General purpose Actix Web error. /// /// An Actix Web error is used to carry errors from `std::error` through actix in a convenient way. /// It can be created through converting errors with `into()`. /// /// Whenever it is created from an external object a response error is created for it that can be /// used to create an HTTP response from it this means that if you have access to an actix `Error` /// you can always get a `ResponseError` reference from it. pub struct Error { cause: Box<dyn ResponseError>, } }
Normally, we write actix-web handler and call dao functions like this:
#![allow(unused)] fn main() { pub async fn get_all_users(data: web::Data<AppState>) -> actix_web::Result<impl Responder> { let db = &data.db; // call dao function to fetch data let users = users::get_all_users(db).await?; Ok(web::Json(users)) } }
If you write a http handler like this and call function in dao(i.e. dao::get_all_users
) function, which returns an error from the dao crate, error will happen.
#![allow(unused)] fn main() { --> src/user/http.rs:348:54 | 348 | let users = users::get_all(db).await?; | ^ the trait `ResponseError` is not implemented for `clickhouse::error::Error` | = help: the following other types implement trait `ResponseError`: AppError BlockingError Box<(dyn StdError + 'static)> HttpError Infallible InvalidHeaderValue JsonPayloadError PathError and 17 others = note: required for `actix_web::Error` to implement `std::convert::From<clickhouse::error::Error>` = note: required for `Result<_, actix_web::Error>` to implement `FromResidual<Result<Infallible, clickhouse::error::Error>>` }
It means that you cannot convert clickhouse::error::Error
to actix_web::Error
.
The reason is that the error type in dao function is clickhouse::error::Error
, not actix_web::Error
. While in http handler, the error type is actix_web::Error
. You have to implement From
trait as rust compiler told you.
Solution
Solution 1: implement From
trait manually
One possible solution is to define application error and implement From
trait, which will convert clickhouse::error::Error
to AppError
:
#![allow(unused)] fn main() { // Define your application error in enum #[derive(Debug)] pub enum AppError { ClickhouseError(ClickhouseError), // ... other error variants } impl ResponseError for AppError { fn error_response(&self) -> HttpResponse { HttpResponse::InternalServerError().body(self.to_string()) // We can also use match to handle specific error define in clickhouse::error::Error // match *self { // AppError::ClickhouseError(ref err) => match err { // ClickhouseError::Timeout(err) => HttpResponse::InternalServerError() // .body(format!("Clickhouse server error: {}", err)), // ClickhouseError::Network(err) => { // HttpResponse::BadRequest().body(format!("Clickhouse client error: {}", err)) // } // _ => HttpResponse::InternalServerError().body("Unknown error"), // }, // ... handle other error variants // } } } use std::fmt; impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { AppError::ClickhouseError(ref err) => { write!(f, "Clickhouse error: {}", err) } // ... handle other error variants } } } impl From<ClickhouseError> for AppError { fn from(error: ClickhouseError) -> Self { AppError::ClickhouseError(error) } } }
Finally, you need to modify actix-web handler:
#![allow(unused)] fn main() { use clickhouse::error::{Error, Result}; // Previous function signature // pub async fn get_all_users(data: web::Data<AppState>) -> actix_web::Result<impl Responder> { // Pass AppError to handler's return type pub async fn get_all_users(data: web::Data<AppState>) -> actix_web::Result<impl Responder, AppError> { let db = &data.db; // call dao function to fetch data let users = users::get_all_users(db).await?; Ok(web::Json(users)) } }
🎉🎉🎉
Solution 2: use thiserror crate
Another solution is to use thiserror crate.
This crate will automatically implement From
trait, which will do the conversion.
#![allow(unused)] fn main() { use thiserror::Error; use clickhouse::error::Error as ClickhouseError; use actix_web::{HttpResponse, ResponseError}; #[derive(Debug, Error)] pub enum AppError { #[error("Clickhouse error: {0}")] ClickhouseError(#[from] ClickhouseError), // You can add more error variants as needed #[error("Database connection error")] DatabaseConnectionError, #[error("Internal server error: {0}")] InternalError(String), } impl ResponseError for AppError { fn error_response(&self) -> HttpResponse { match self { AppError::ClickhouseError(_) => { HttpResponse::InternalServerError().body(self.to_string()) } AppError::DatabaseConnectionError => { HttpResponse::ServiceUnavailable().body(self.to_string()) } AppError::InternalError(_) => { HttpResponse::InternalServerError().body(self.to_string()) } } } } }
Here is the key improvements with thiserror
:
- The
#[derive(Error)]
automatically implementsstd::error::Error
. #[error("...")]
provides a convenient way to implementDisplay
trait.#[from]
attribute automatically implementsFrom
trait for error conversion.- The code is more concise and readable.
- You can easily add more error variants with custom error messages.
Benefits of this approach:
- Automatic error conversion
- Clear, descriptive error messages
- Easy to extend with new error types
- Consistent error handling across the application
The ResponseError
implementation allows you to:
- Map different error types to appropriate HTTP status codes
- Provide meaningful error responses
- Easily customize error handling for different error variants
Note: Make sure to import necessary types and traits from the appropriate modules (actix-web, thiserror, etc.).
Below is the example of using thiserror crate:
// use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder}; use actix_web::{ web, App, HttpServer, Responder}; use clickhouse::Client; use clickhouse_example::{dao::get_all_users, error::AppError}; // This struct represents state pub(crate) struct AppState { pub app_name: String, pub db: Client, } // NOTE: This function is not working because of error type mismatch // async fn get_users( // data: web::Data<AppState>, // ) -> actix_web::Result<impl Responder> { // let db = &data.db; // // call dao function to fetch data // let users = get_all_users(db).await?; // Ok(web::Json(users)) // } // Handler function pub(crate) async fn get_users(data: web::Data<AppState>) -> actix_web::Result<impl Responder, AppError> { let db = &data.db; // call dao function to fetch data let users = get_all_users(db).await?; Ok(web::Json(users)) } #[actix_web::main] async fn main() -> std::io::Result<()> { let url = "http://localhost:8123"; let database = "default"; let user = "test"; let password = "secret"; let client = Client::default() .with_url(url) .with_user(user) .with_password(password) .with_database(database); HttpServer::new(move || { App::new() .app_data(web::Data::new(AppState {db:client.clone(), app_name: "My App".into() })) .route("/users", web::get().to(get_users)) }) .bind(("127.0.0.1", 8080))? .run() .await }