Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Error Handling in Rust (Actix Web)
Praveen Chaudhary
Praveen Chaudhary

Posted on • Edited on

     

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 typeResult<T, E> andUnrecoverable errors havepanic! 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).
InternalAt the edge
Control FlowTypes, methods, fieldsStatus codes
ReportingLogs/tracesResponse body

You can refer morehere

The first place where we are likely to get an error ispool.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 modeExit 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 simpleResultSignal enum

'./src/error.rs'

pub enum ResultSignal<Success> {    Ok(Success),    Err}
Enter fullscreen modeExit 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 modeExit fullscreen mode

Then we need to implement theDebug and Display trait. It is used to print errors usingprintln 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 modeExit fullscreen mode
  • #[derive(Debug)] : This macro will enable the Debug trait for the enum AppErrorType.

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

  • write! andwriteln! 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 theFrom 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 modeExit fullscreen mode

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

Actix provide two methodserror_response andstatus_code to handle errors response.

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

Then, we need to change the return type fromimpl Responder orHttpResponse toResult.

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 modeExit fullscreen mode

Instead of using unwrap inclient.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 tooResult<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 modeExit fullscreen mode

Let's spin our server and hit the endpoint.

Returned Response
The request returned a500 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 modeExit 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 modeExit fullscreen mode

Once we are done withdisplay anddebug 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 modeExit fullscreen mode

In the above code, we have usedstatus_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 modeExit 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 modeExit 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 modeExit fullscreen mode

Make sure to enable the logger to get a vivid description. You can use theactix default logger or theslog logger. You can read more about sloghere.

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 modeExit 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 modeExit 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 herechaudharypraveen98 - 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!

Top comments(6)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
the_unsender profile image
SwitchedToGitlab
Rust, SIP, OSS, and bomber gear.
  • Education
    Self-Taught
  • Work
    Freelancer
  • Joined

Minor typo:

Change:

Errors are the part of software development.

To:

Errors are part of software development.

For the English as a Second (or third, etc) Language readers:

"foo is part of bar" is an English phrase meaning that foo is an essential component of the act of bar.

This is one of the better explanations of error handling and types I've seen.

Much appreciated!

CollapseExpand
 
chaudharypraveen98 profile image
Praveen Chaudhary
SSE at Infoedge | Prev - SDE 2 - Sigmoid, SDE at India Today| Rust🦀 and Python🐍 enthusiast | Full stack developer | API & App developer| Performance & Acceptance Tester
  • Location
    Faridabad, Haryana, India
  • Education
    Guru Gobind Singh Indraprastha University
  • Pronouns
    He/Him
  • Work
    SSE at InfoEdge
  • Joined

Thank you so much for the feedback. I have noticed there are other typos. I have fixed them.

CollapseExpand
 
beastabhi199 profile image
Abhishek Chaudhary🇮🇳
  • Education
    Bcom (Hons)
  • Work
    CA Aspirant
  • Joined

Ver Nice.. 👍

CollapseExpand
 
javadbat profile image
mohammad javad bathaei
  • Joined

where do i use AppErrorResponse type you use in your content code?

CollapseExpand
 
javadbat profile image
mohammad javad bathaei
  • Joined

i follow your souce and find this code in example github repo
github.com/nemesiscodex/actix-todo...
that it defined like this:

#[derive(Serialize)]pub struct AppErrorResponse {    pub error: String,}
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
chaudharypraveen98 profile image
Praveen Chaudhary
SSE at Infoedge | Prev - SDE 2 - Sigmoid, SDE at India Today| Rust🦀 and Python🐍 enthusiast | Full stack developer | API & App developer| Performance & Acceptance Tester
  • Location
    Faridabad, Haryana, India
  • Education
    Guru Gobind Singh Indraprastha University
  • Pronouns
    He/Him
  • Work
    SSE at InfoEdge
  • Joined

It is used in the "ResponseError" trait for the "error_response" methodgithub.com/chaudharypraveen98/acti...

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

SSE at Infoedge | Prev - SDE 2 - Sigmoid, SDE at India Today| Rust🦀 and Python🐍 enthusiast | Full stack developer | API & App developer| Performance & Acceptance Tester
  • Location
    Faridabad, Haryana, India
  • Education
    Guru Gobind Singh Indraprastha University
  • Pronouns
    He/Him
  • Work
    SSE at InfoEdge
  • Joined

More fromPraveen Chaudhary

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp