Error Handling in Rust (Actix Web)

chaudharypraveen98

Praveen Chaudhary

Posted on June 14, 2022

Error Handling in Rust (Actix Web)

Errors are part of software development. There are basically two types of errors in Rust i.e recoverable and unrecoverable.

Recoverable errors have type Result<T, E> and Unrecoverable errors have panic! macro that stops execution.

Errors serve two main purposes:

  • Control flow (i.e. determine what to do next);
  • Reporting (e.g. investigating, after the fact, what went wrong).

We can also distinguish errors based on their location:

  • Internal (i.e. a function calling another function within our application);
  • At the edge (i.e. an API request that we failed to fulfill).
Internal At the edge
Control Flow Types, methods, fields Status codes
Reporting Logs/traces Response body

You can refer more here

The first place where we are likely to get an error is pool.get() . When we are unable to get the pool instance whatever will be the reason like wrong credentials, database instance not running, etc.

Let's try how to handle it.

Using Unwrap to create panic.

Creating Panic is useful during the prototyping phase when we are more focused on logic implementation.

'/src/api_handlers.rs'



pub async fn get_tags(state: web::Data<AppState>) ->impl Responder  {
    // Just not handle the error and let the system to Panic (unrecoverable error)
    let client = state.pool
        .get()
        .await.unwrap();
    let result = db::get_tags_unwrap(&client).await;

    match result {
        Ok(tags) => HttpResponse::Ok().json(tags),
        Err(_) => HttpResponse::InternalServerError().into(),
    }
}


Enter fullscreen mode Exit fullscreen mode

Calling unwrap will create panic and the execution stops. So, we have discovered that handling errors like this are not ideal in the production server.

Let's move to another way

Using the Result

Handling Single Error

The caller of the execution must be aware of whether the program was completed successfully or failed. For this use case, we can use simple ResultSignal enum

'./src/error.rs'



pub enum ResultSignal<Success> {
    Ok(Success),
    Err
}


Enter fullscreen mode Exit fullscreen mode

It will return Ok status on success and an Error on failure. It is helpful now that our user is aware that something mishappen has occurred. It is suitable for a single kind of error but our system consists of different services and they can fail in different ways.

what is the reason? where has failure occurred?
To answer this, we need to handle various errors according to our needs like database errors, filter errors, etc

Handling Multiple Errors

Let's create an enum for different types of errors. For the sake of simplicity, we just considering two cases only Db Error and Not Found Error

'./src/error.rs'



pub enum AppErrorType {
    DbError,
    NotFoundError,
}


Enter fullscreen mode Exit fullscreen mode

Then we need to implement the Debug and Display trait. It is used to print errors using println command.



#[derive(Debug)]
pub enum AppErrorType {
    DbError,
    NotFoundError,
}

impl fmt::Display for AppErrorType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}


Enter fullscreen mode Exit fullscreen mode
  • #[derive(Debug)] : This macro will enable the Debug trait for the enum AppErrorType.

  • fn fmt : Display is for user-facing output.

  • write! and writeln! are two macros that are used to emit the format string to a specified stream. This is used to prevent intermediate allocations of format strings and instead directly write the output.

Let's implement the From trait to convert from one type to another like PoolError to AppErrorType.



impl From<PoolError> for AppErrorType {
    fn from(_error: PoolError) -> AppErrorType {
        AppErrorType::DbError
    }
}
impl From<Error> for AppErrorType {
    fn from(_error: Error) -> AppErrorType {
        AppErrorType::DbError
    }
}


Enter fullscreen mode Exit fullscreen mode

Since, we will be using Result return type in api handlers then we need overwrite the ResponseError Trait.

Actix provide two methods error_response and status_code to handle errors response.



impl ResponseError for AppErrorType {
    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code()).finish()
    }
}


Enter fullscreen mode Exit fullscreen mode

Then, we need to change the return type from impl Responder or HttpResponse to Result.

We have used the ? trait already implemented above in error.rs instead to unwrap.

'./src/api_handlers.rs'



pub async fn get_tags(state: web::Data<AppState>) ->Result<HttpResponse,AppErrorType> {
    let client: Client = state.pool.get().await?;
    let result = db::get_tags(&client).await;
    result.map(|tags| HttpResponse::Ok().json(tags))
}


Enter fullscreen mode Exit fullscreen mode

Instead of using unwrap in client.prepare("select * from tag limit 10;").await.unwrap(), we can now use the ? as we have implemented the From trait and update the return type too Result<Vec<Tag>, AppErrorType>

'./src/db.rs'



pub async fn get_tags(client: &Client) -> Result<Vec<Tag>, AppErrorType> {
    let statement = client.prepare("select * from tag limit 10;").await?;
    let tags = client
        .query(&statement, &[])
        .await
        .expect("Error getting tags")
        .iter()
        .map(|row| Tag::from_row_ref(row).unwrap())
        .collect::<Vec<Tag>>();

    Ok(tags)
}


Enter fullscreen mode Exit fullscreen mode

Let's spin our server and hit the endpoint.

Returned Response
The request returned a 500 status code. Instead of just panicking we are getting a status code that will help to debug.

But is only status code really helpful??

Not, not at all, A good error contains the cause of the error, the error status code, and a message for the client user which is human readable

Let's try to improve our error handling

Let's implement the AppError struct which contains our three fields' cause, error type, and message.

'./src/error.rs'



pub struct AppError {
    pub cause: Option<String>,
    pub message: Option<String>,
    pub error_type: AppErrorType,
}


Enter fullscreen mode Exit fullscreen mode

Just like AppErrorType, Let's implement Debug and Display trait



#[derive(Debug)]
pub struct AppError {
    pub cause: Option<String>,
    pub message: Option<String>,
    pub error_type: AppErrorType,
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}


Enter fullscreen mode Exit fullscreen mode

Once we are done with display and debug trait, let's define ResponseError for AppError



impl ResponseError for AppError {

    fn status_code(&self) -> StatusCode {
        match self.error_type {
            AppErrorType::DbError => (StatusCode::INTERNAL_SERVER_ERROR),
            AppErrorType::NotFoundError => (StatusCode::NOT_FOUND),
        }
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code()).json(AppErrorResponse {
            error: self.message(),
        })
    }
}


Enter fullscreen mode Exit fullscreen mode

In the above code, we have used status_code to match different errors and provide a status code according to it.

As soon the ResponseError is defined, We use From trait for error type conversion



impl From<PoolError> for AppError {
    fn from(error: PoolError) -> AppError {
        AppError {
            message: None,
            cause: Some(error.to_string()),
            error_type: AppErrorType::DbError,
        }
    }
}
impl From<Error> for AppError {
    fn from(error: Error) -> AppError {
        AppError {
            message: None,
            cause: Some(error.to_string()),
            error_type: AppErrorType::DbError,
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Let's implement a default message for the error types.



impl AppError {
    // we are handling the none. the function name should match the field name
    fn message(&self) -> String {
        match &*self {
            // Error message is found then clone otherwise default message
            AppError {
                cause: _,
                message: Some(message),
                error_type: _,
            } => message.clone(),
            AppError {
                cause: _,
                message: None,
                error_type: AppErrorType::NotFoundError,
            } => "The requested item was not found".to_string(),
            _ => "An unexpected error has occurred".to_string(),
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

We are done with all the necessary changes in error.rs file. Let's start with API handlers and DB handlers

'./src/api_handlers.rs'



pub async fn get_tags(state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
    let client: Client = state.pool.get().await?;
    let result = db::get_tags(&client).await;
    result.map(|tags| HttpResponse::Ok().json(tags))
}


Enter fullscreen mode Exit fullscreen mode

Make sure to enable the logger to get a vivid description. You can use the actix default logger or the slog logger. You can read more about slog here.



async fn configure_pool(pool: Pool, log: Logger) -> Result<Client, AppError> {
    pool.get().await.map_err(|err| {
        let sublog = log.new(o!("cause"=>err.to_string()));
        crit!(sublog, "Error creating client");
        AppError::from(err)
    })
}


pub async fn get_tags(state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
    let sublog = state.log.new(o!("handler" => "get_tags"));
    let client: Client = configure_pool(state.pool.clone(), sublog.clone()).await?;
    let result = db::get_tags(&client).await;
    result.map(|tags| HttpResponse::Ok().json(tags))
}


Enter fullscreen mode Exit fullscreen mode

Let's change the Db handler file.

'./src/db.rs'



pub async fn get_tags(client: &Client) -> Result<Vec<Tag>, AppError> {
    let statement = client.prepare("select * from tag limit 10;").await?;
    let tags = client
        .query(&statement, &[])
        .await
        .expect("Error getting tags")
        .iter()
        .map(|row| Tag::from_row_ref(row).unwrap())
        .collect::<Vec<Tag>>();

    Ok(tags)
}


Enter fullscreen mode Exit fullscreen mode

Let's start our server and hit the API endpoint.

Client side error

Client side error

Server side error

Server side error

Wow!! We have now logs and error messages for the client user.

For Awesome logs, we have used the slog logger. If you want to follow my blog then checkout here chaudharypraveen98 - Adding Slog Logger to Actix-web.

Source Code

GitHub Source Code

References

Added Comments for your quick revision and understanding.

Feel free to ask any questions or provide suggestions. I am too learning. So will be glad to get your feedback.

Happy Hacking
Rustaceans!

💖 💪 🙅 🚩
chaudharypraveen98
Praveen Chaudhary

Posted on June 14, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related