TutorialintermediatePart 16 of 18

Error Handling the Rust Way

Stop returning generic 500 errors and start providing useful feedback.

April 27, 20263 min read

What you'll learn

  • Defining a custom ErrorCode enum
  • Creating a ResponseError struct
  • Using Result and ? for cleaner handlers
Prerequisites:Previous post completed (Environment Variables and Stages)

Rust is famous for its error handling. It doesn't use exceptions; instead, it uses the Result type. This forces you to acknowledge that things can go wrong.

In a serverless API, you want to translate these Rust errors into meaningful HTTP status codes (400 for bad input, 401 for unauthorized, etc.).

Error structure

Let's build a structured error system in our library (src/lib.rs) that matches the production patterns we've seen.

Defining Error Codes

First, define an enum for all the things that can go wrong in your app.

src/lib.rs
rust
pub enum ErrorCode {
    BadRequest,
    NotFound,
    AccessDenied,
    UnknownError,
}
 
impl ErrorCode {
    pub fn message(&self) -> &'static str {
        match self {
            ErrorCode::BadRequest => "Bad request, please check your input",
            ErrorCode::NotFound => "The requested resource was not found",
            ErrorCode::AccessDenied => "You do not have permission to do this",
            ErrorCode::UnknownError => "An unexpected error occurred",
        }
    }
 
    pub fn status_code(&self) -> u16 {
        match self {
            ErrorCode::BadRequest => 400,
            ErrorCode::NotFound => 404,
            ErrorCode::AccessDenied => 403,
            ErrorCode::UnknownError => 500,
        }
    }
}

The ResponseError struct

This is what we'll actually use to build our JSON responses.

src/lib.rs
rust
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ResponseError {
    pub status_code: u16,
    pub code: String,
    pub message: String,
}
 
impl ResponseError {
    pub fn new(error: ErrorCode, custom_message: Option<&str>) -> Self {
        Self {
            status_code: error.status_code(),
            code: format!("{:?}", error).to_uppercase(), // e.g., "BADREQUEST"
            message: custom_message.unwrap_or(error.message()).to_string(),
        }
    }
}

Using it in the Handler

Now, your handler code becomes much more readable. You can use the ? operator or match to handle errors gracefully.

Update your src/bin/GreetingManager.rs:

src/bin/GreetingManager.rs
rust
use rust_serverless_api::{ErrorCode, ResponseError};
 
async fn handler(event: LambdaEvent<ApiGatewayProxyRequest>) -> Result<ApiGatewayProxyResponse, Error> {
    // ... logic ...
 
    if some_validation_failed {
        let err = ResponseError::new(ErrorCode::BadRequest, Some("Name is too short"));
        return create_api_error_response(err); // A helper that returns the right status code
    }
 
    // ...
}

Integration with API Gateway

Remember lesson 5 where we set up those GatewayResponse resources in template.yaml?

  • If your Rust code returns a 403, it follows our standard format.
  • If API Gateway itself blocks a request (like a timeout or missing auth), it uses the GatewayResponse format we defined.

Because both formats match, the client always gets a consistent JSON object, no matter who generated the error.

The Power of the ? Operator

As you add more complex logic (like database calls), you'll appreciate Rust's ? operator. It allows you to "bubble up" errors without writing dozens of nested if statements.

rust
Source File
let user = table.get_user(id).await?; // If this fails, the whole function returns an error immediately

Info

In a real project, you'd also implement From<SomeOtherError> for ResponseError so that third-party library errors (like from DynamoDB) automatically turn into your custom API errors.

We've covered a lot of ground. Our code is robust, organized, and scalable. But as we add more files, sam build starts to feel a bit slow and manual. In the next post, we'll learn how to use a Makefile to automate our build process like a pro.