Praveen Chaudhary
Posted on June 14, 2022
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.
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(),
}
}
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
pub enum ResultSignal<Success> {
Ok(Success),
Err
}
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
pub enum AppErrorType {
DbError,
NotFoundError,
}
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)
}
}
#[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
}
}
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()
}
}
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.
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))
}
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>
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)
}
Let's spin our server and hit the endpoint.
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.
pub struct AppError {
pub cause: Option<String>,
pub message: Option<String>,
pub error_type: AppErrorType,
}
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)
}
}
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(),
})
}
}
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,
}
}
}
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(),
}
}
}
We are done with all the necessary changes in error.rs file. Let's start with API handlers and DB handlers
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))
}
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))
}
Let's change the Db handler file.
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)
}
Let's start our server and hit the API endpoint.
Client 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
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!
Posted on June 14, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.