ESC
Type to search...

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