Error Handling the Rust Way
Stop returning generic 500 errors and start providing useful feedback.
What you'll learn
- Defining a custom ErrorCode enum
- Creating a ResponseError struct
- Using Result and ? for cleaner handlers
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.
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.
#[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:
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
GatewayResponseformat 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.
let user = table.get_user(id).await?; // If this fails, the whole function returns an error immediatelyInfo
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.