Error Handling
RustAPI ships with a structured ApiError type and a consistent wire format for error responses. The trick is not just returning errors, but returning the right error to the client while keeping internal details out of production responses.
Problem
Without a clear error strategy, handlers tend to mix:
- business errors,
- validation errors,
- infrastructure errors, and
- internal debugging details.
That usually leads to noisy handlers and accidental leakage of sensitive information.
Solution
Use ApiError at the HTTP boundary and convert your domain/application errors into it.
Basic handler pattern
#![allow(unused)]
fn main() {
use rustapi_rs::prelude::*;
#[derive(Serialize, Schema)]
struct UserDto {
id: u64,
email: String,
}
#[rustapi_rs::get("/users/{id}")]
async fn get_user(Path(id): Path<u64>) -> Result<Json<UserDto>> {
if id == 0 {
return Err(ApiError::bad_request("id must be greater than zero"));
}
let user = find_user(id)
.await?
.ok_or_else(|| ApiError::not_found(format!("User {} not found", id)))?;
Ok(Json(user))
}
async fn find_user(_id: u64) -> Result<Option<UserDto>> {
Ok(None)
}
}
Mapping application errors into ApiError
#![allow(unused)]
fn main() {
use rustapi_rs::prelude::*;
#[derive(Debug)]
enum AppError {
UserNotFound(u64),
DuplicateEmail,
Storage(std::io::Error),
}
impl From<AppError> for ApiError {
fn from(err: AppError) -> Self {
match err {
AppError::UserNotFound(id) => {
ApiError::not_found(format!("User {} not found", id))
}
AppError::DuplicateEmail => {
ApiError::conflict("A user with that email already exists")
}
AppError::Storage(source) => {
ApiError::internal("Storage error").with_internal(source.to_string())
}
}
}
}
#[derive(Serialize, Schema)]
struct UserDto {
id: u64,
email: String,
}
#[rustapi_rs::get("/users/{id}")]
async fn get_user(Path(id): Path<u64>) -> Result<Json<UserDto>> {
let user = load_user(id).await?;
Ok(Json(user))
}
async fn load_user(id: u64) -> std::result::Result<UserDto, AppError> {
if id == 42 {
return Err(AppError::UserNotFound(id));
}
Ok(UserDto {
id,
email: "demo@example.com".into(),
})
}
}
Validation errors are already normalized
#![allow(unused)]
fn main() {
use rustapi_rs::prelude::*;
#[derive(Deserialize, Validate, Schema)]
struct CreateUser {
#[validate(email)]
email: String,
#[validate(length(min = 8))]
password: String,
}
#[rustapi_rs::post("/users")]
async fn create_user(ValidatedJson(body): ValidatedJson<CreateUser>) -> Result<StatusCode> {
let _ = body;
Ok(StatusCode::CREATED)
}
}
If validation fails, RustAPI returns 422 Unprocessable Entity automatically.
Error response shape
RustAPI serializes errors as JSON like this:
{
"error": {
"type": "not_found",
"message": "User 42 not found"
},
"error_id": "err_a1b2c3d4e5f6..."
}
Validation errors add fields:
{
"error": {
"type": "validation_error",
"message": "Request validation failed",
"fields": [
{
"field": "email",
"code": "email",
"message": "must be a valid email"
}
]
},
"error_id": "err_a1b2c3d4e5f6..."
}
Discussion
Use 4xx for client-facing corrections
Good candidates for direct client messages:
bad_requestunauthorizedforbiddennot_foundconflict- validation failures
Use 5xx for internal failures
For infrastructure or unexpected failures, prefer ApiError::internal(...) and attach private details with .with_internal(...).
That gives operators useful logs without sending those internals to clients.
Production masking
When RUSTAPI_ENV=production, server-side error messages are masked automatically.
Example:
- development 500 message:
Storage error - production 500 message:
An internal error occurred
Validation field details still remain visible.
Error correlation
Every response includes an error_id. Use it to correlate:
- client reports,
- server logs,
- trace/span data,
- audit or replay workflows.
SQLx integration
When the SQLx feature is enabled, sqlx::Error converts into ApiError automatically. That means ? works naturally in many handlers while still mapping common database failures to sensible HTTP responses.
Testing
Manual checks:
curl -i http://127.0.0.1:8080/users/0
curl -i http://127.0.0.1:8080/users/42
curl -i -X POST http://127.0.0.1:8080/users -H "content-type: application/json" --data "{\"email\":\"bad\",\"password\":\"123\"}"
What to verify:
400returns abad_requesterror body404returns anot_founderror body422returnsfieldsentries- every error payload contains
error_id