Skip to main content
Lesson 9 of 12Beginner⏱ 10 mins

Handling Errors

Custom errors and Result.

67% of path

Learning Objectives

  • Return `Result` from handlers

##Turning failures into good responses

Things go wrong: a user does not exist, the database is down, the input is invalid. A good API turns each failure into a clear HTTP response instead of crashing. Actix Web makes this natural with Rust's Result type.

#Handlers can return Result

If a handler returns Result<T, E>, Actix sends T on success and converts E into an error response on failure. The only requirement is that E implements the ResponseError trait.

Actix already implements it for several types. The quickest option is the helper functions in actix_web::error:

code_blocksrust
use actix_web::{error, get, web, Result};

#[get("/users/{id}")]
async fn get_user(path: web::Path<u32>) -> Result<String> {
    let id = path.into_inner();
    if id == 0 {
        return Err(error::ErrorBadRequest("id must be greater than zero"));
    }
    if id > 100 {
        return Err(error::ErrorNotFound("no such user"));
    }
    Ok(format!("User #{id}"))
}

#Your own error type

For real applications define one error enum that describes everything that can fail, then implement ResponseError so each variant maps to a status code. The derive_more crate removes most of the boilerplate:

code_blocksrust
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
enum ApiError {
    #[display("not found")]
    NotFound,
    #[display("invalid input: {_0}")]
    BadRequest(#[error(not(source))] String),
    #[display("internal error")]
    Internal,
}

impl ResponseError for ApiError {
    fn status_code(&self) -> StatusCode {
        match self {
            ApiError::NotFound => StatusCode::NOT_FOUND,
            ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
            ApiError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code()).json(serde_json::json!({
            "error": self.to_string()
        }))
    }
}

Now handlers can return Result<HttpResponse, ApiError> and use the ? operator freely:

code_blocksrust
async fn find(id: u32) -> Result<String, ApiError> {
    if id == 0 { return Err(ApiError::BadRequest("id must be positive".into())); }
    Ok(format!("user {id}"))
}

#Do not leak internals

Log the detailed error on the server, but send clients a short, safe message. Never return raw database errors or stack traces in a response, because they reveal how your system works.

#Extractor errors

When JSON or query parsing fails, Actix returns a plain 400 by default. You can customize this with web::JsonConfig::default().error_handler(...) so even parse errors use your JSON error format.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

See the default error response.

Starting Code
Err(error::ErrorNotFound("no such user"))
Suggested Modification

Change ErrorNotFound to ErrorBadRequest and request the endpoint with curl -i.

Expected Result

The status line changes from 404 Not Found to 400 Bad Request, with your message as the body.

Explanation: The helper functions wrap your message in an error that already implements ResponseError with the matching status code.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Using unwrap() in handlers

It is quick to write, but any failure panics the worker and returns a 500.

Incorrect Pattern
let user = db.find(id).await.unwrap();
Correct Pattern
let user = db.find(id).await.map_err(|_| ApiError::NotFound)?;

Explanation: Convert the failure into your error type with map_err and let ? return it as a proper HTTP response.

!

Leaking internal error details

Returning err.to_string() from the database exposes table names and queries.

Incorrect Pattern
HttpResponse::InternalServerError().body(err.to_string())
Correct Pattern
log::error!("db error: {err}"); HttpResponse::InternalServerError().json(serde_json::json!({ "error": "internal error" }))

Explanation: Record the full error in your logs for yourself, and give the client only a generic message.

Practical Exercise

Create a custom error type

Task

Write an ApiError with NotFound and BadRequest variants and use it in a handler for GET /items/{id}.

Requirements

  • Implement ResponseError for ApiError
  • Return 404 when id is 99 and 400 when id is 0
  • Return the id as text otherwise

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

Production APIs define one error enum per service (often with thiserror), convert database and validation errors into it with From, and return a consistent JSON error shape that frontend teams can rely on.

Key Takeaways

  • Return Result<T, E> from handlers and use ? to propagate errors.
  • Implement ResponseError on your own enum to map each variant to a status code.
  • actix_web::error::ErrorNotFound and friends are quick shortcuts for simple cases.
  • Log details internally and send safe, short messages to clients.

Finished reading?

Mark this lesson as done to track your progress and earn XP.