Error Handling in Rust with Actix Web: Best Practices and Strategies

Naveenkumar Karthigayan
3 min readMar 27, 2024

--

Error handling is a critical aspect of writing robust and reliable software in Rust. Actix Web, a high-performance web framework for Rust, provides developers with powerful tools and idioms for managing errors effectively. In this guide, we’ll delve into advanced error handling techniques using custom error types and response formatting in Actix Web, ensuring your Rust applications handle errors gracefully.

Dependency

[dependencies]
thiserror = "1.0.58"
serde_json = "1.0.114"
tokio = { version = "1.36.0", features = ["full"] }
bcrypt = "0.15.1"
actix = "0.13.3"
actix-web = "4.5.1"
actix-xml = "0.2.0"
quick-xml = { version = "0.31.0", features = ["serialize"] }
serde = { version = "1.0.197", features = ["derive"] }

Custom Error Types

We begin by defining custom error types to encapsulate various error scenarios within our Rust applications. These custom error types enhance clarity and allow for better organization of error handling logic.


use actix_web::dev::{Service};
use bcrypt::BcryptError;
use tokio::task::JoinError;
use actix_web::http::StatusCode;
use actix_web::{HttpResponse};
use quick_xml::se::to_string;
use serde::{Deserialize, Serialize};


#[derive(thiserror::Error, Debug)]
#[error("...")]
pub enum Error {

#[error("Error parsing ObjectID {0}")]
ParseObjectID(String),

#[error("{0}")]
Authenticate(#[from] AuthenticateError),

#[error("{0}")]
BadRequest(#[from] BadRequest),

#[error("{0}")]
NotFound(#[from] NotFound),

#[error("{0}")]
RunSyncTask(#[from] JoinError),

#[error("{0}")]
HashPassword(#[from] BcryptError),
}

impl Error {
fn get_codes(&self) -> (StatusCode, u16) {
match *self {
// 4XX Errors
Error::ParseObjectID(_) => (StatusCode::BAD_REQUEST, 40001),
Error::BadRequest(_) => (StatusCode::BAD_REQUEST, 40002),
Error::NotFound(_) => (StatusCode::NOT_FOUND, 40003),
Error::Authenticate(AuthenticateError::WrongCredentials) => (StatusCode::UNAUTHORIZED, 40004),
Error::Authenticate(AuthenticateError::InvalidToken) => (StatusCode::UNAUTHORIZED, 40005),
Error::Authenticate(AuthenticateError::Locked) => (StatusCode::LOCKED, 40006),

// 5XX Errors
Error::Authenticate(AuthenticateError::TokenCreation) => {
(StatusCode::INTERNAL_SERVER_ERROR, 5001)
}

Error::RunSyncTask(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5005),
Error::HashPassword(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5006),
}
}


pub fn bad_request(message : String) -> Self {
Error::BadRequest(BadRequest {message})
}

pub fn not_found() -> Self {
Error::NotFound(NotFound {})
}
}


#[derive(thiserror::Error, Debug)]
#[error("...")]
pub enum AuthenticateError {
#[error("Wrong authentication credentials")]
WrongCredentials,
#[error("Failed to create authentication token")]
TokenCreation,
#[error("Invalid authentication credentials")]
InvalidToken,
#[error("User is locked")]
Locked,
}

#[derive(thiserror::Error, Debug)]
#[error("Bad Request {message}")]
pub struct BadRequest {
message : String
}

#[derive(thiserror::Error, Debug)]
#[error("Not found")]
pub struct NotFound {}

Generating Error Responses

Once we have defined our custom error types, we can implement methods to generate appropriate HTTP responses for different error scenarios.

#[derive(Serialize,Deserialize)]
pub struct ErrorResponse {
code : String,
message : String
}

impl Error {
pub fn error_response(&self) -> HttpResponse {
let status_code = self.get_status_code();
let message = self.to_string();
let code = status_code.as_u16();
let error_response = ErrorResponse { code, message };
HttpResponse::build(status_code).json(error_response)
}

pub fn error_response_xml(&self) -> HttpResponse {
let status_code = self.get_status_code();
let message = self.to_string();
let code = status_code.as_u16();
let error_response = ErrorResponse { code, message };
let error_resp_string = match to_string(&error_response) {
Ok(xml_string) => xml_string,
Err(_) => "<error>".to_string(), // Handle serialization error gracefully
};
HttpResponse::build(status_code).content_type("application/xml").body(error_resp_string)
}
}

Usage Example

Let’s see how we can use these error handling utilities in our Actix Web handlers:

use std::io::Read;
use actix_web::{App, HttpRequest, HttpServer, Responder, ResponseError, web};
use actix_web::body::MessageBody;
use crate::errors::{AuthenticateError, Error};

mod errors;


pub async fn send_authenticate_error(_req: HttpRequest) -> impl Responder {
// This API will send auth error as response in JSON format
Error::Authenticate(AuthenticateError::WrongCredentials).error_response()
}


pub async fn send_bad_request(_req: HttpRequest) -> impl Responder {
// This API will send bad request error as response inXMl format
Error::bad_request("Value is required".to_string()).error_response_xml()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/auth_error", web::get().to(send_authenticate_error))
.route("/bad_request", web::get().to(send_bad_request))
}).bind("0.0.0.0:8000")?
.run()
.await
}

Conclusion

Custom error types and response formatting are powerful tools for handling errors effectively in Rust applications with Actix Web. By defining clear error semantics and providing informative error responses, we can enhance the reliability and usability of our web services.

Contact Details

For inquiries or collaborations, feel free to contact me:
- Email: naveenkumarkarthigayan@gmail.com
- LinkedIn: https://www.linkedin.com/in/naveenkumar001/

--

--