Error Handling

Standardized error responses with trace IDs

Rapina provides standardized error handling with consistent response formats and trace IDs for debugging.

Error Response Format

All errors return a consistent JSON envelope:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "user not found"
  },
  "trace_id": "550e8400-e29b-41d4-a716-446655440000"
}

The trace_id is automatically generated for each request and can be used to correlate logs and debug issues.

Built-in Error Constructors

Error::bad_request("invalid input")      // 400
Error::unauthorized("login required")    // 401
Error::forbidden("access denied")        // 403
Error::not_found("user not found")       // 404
Error::conflict("already exists")        // 409
Error::validation("invalid email")       // 422
Error::rate_limited("too many requests") // 429
Error::internal("something went wrong")  // 500

Using Errors in Handlers

Return Result<T, Error> or just Result<T> from handlers:

#[get("/users/:id")]
async fn get_user(id: Path<u64>) -> Result<Json<User>> {
    let id = id.into_inner();

    if id == 0 {
        return Err(Error::bad_request("id cannot be zero"));
    }

    let user = find_user(id)
        .ok_or_else(|| Error::not_found("user not found"))?;

    Ok(Json(user))
}

Adding Details

Add structured details to errors:

Error::validation("invalid input")
    .with_details(serde_json::json!({
        "field": "email",
        "reason": "invalid format"
    }))

Response:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "invalid input",
    "details": {
      "field": "email",
      "reason": "invalid format"
    }
  },
  "trace_id": "..."
}

Domain Errors

Define typed domain errors with automatic API conversion:

enum UserError {
    NotFound(u64),
    EmailTaken(String),
}

impl IntoApiError for UserError {
    fn into_api_error(self) -> Error {
        match self {
            UserError::NotFound(id) => Error::not_found(format!("user {} not found", id)),
            UserError::EmailTaken(email) => Error::conflict(format!("email {} taken", email)),
        }
    }
}

Use with the ? operator:

#[get("/users/:id")]
async fn get_user(id: Path<u64>) -> Result<Json<User>, UserError> {
    let id = id.into_inner();
    let user = find_user(id).ok_or(UserError::NotFound(id))?;
    Ok(Json(user))
}

Documented Errors

Document error responses for OpenAPI generation:

use rapina::prelude::*;

struct GetUserHandler;

impl DocumentedError for GetUserHandler {
    fn error_responses() -> Vec<ErrorVariant> {
        vec![
            ErrorVariant::new(400, "BAD_REQUEST", "Invalid user ID"),
            ErrorVariant::new(404, "NOT_FOUND", "User not found"),
        ]
    }
}

Error Codes

HTTP StatusCodeUse Case
400BAD_REQUESTInvalid input, malformed request
401UNAUTHORIZEDMissing or invalid authentication
403FORBIDDENAuthenticated but not allowed
404NOT_FOUNDResource doesn't exist
409CONFLICTResource already exists
422VALIDATION_ERRORInput validation failed
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORServer error