Tratamento de Erros em Handlers
Mapeie falhas de domínio para respostas HTTP consistentes com IntoResponse, thiserror e ? em handlers Axum.
Receita
Cartão de referência rápida - pronto para copiar e colar.
use axum::{http::StatusCode, response::IntoResponse, Json};
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("não encontrado")] NotFound,
#[error("{0}")] BadRequest(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, msg) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
};
(status, Json(serde_json::json!({ "error": msg }))).into_response()
}
}
async fn handler() -> Result<Json<Data>, AppError> { /* ... */ }Quando usar isso: Toda API de produção precisa de um tipo de erro (ou enum) que converta para JSON e códigos de status estáveis.
Exemplo Funcional
use axum::{http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize;
use thiserror::Error;
#[derive(Serialize)]
struct ErrorBody { code: &'static str, message: String }
#[derive(Error, Debug)]
enum ApiError {
#[error("item não encontrado")] NotFound,
#[error("id inválido: {0}")] InvalidId(String),
#[error(transparent)] Db(#[from] sqlx::Error),
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let (status, code, message) = match &self {
ApiError::NotFound => (StatusCode::NOT_FOUND, "not_found", self.to_string()),
ApiError::InvalidId(_) => (StatusCode::BAD_REQUEST, "invalid_id", self.to_string()),
ApiError::Db(_) => (StatusCode::INTERNAL_SERVER_ERROR, "db_error", "erro interno".into()),
};
(status, Json(ErrorBody { code, message })).into_response()
}
}
async fn get_item(axum::extract::Path(id): axum::extract::Path<String>) -> Result<Json<serde_json::Value>, ApiError> {
if id.is_empty() {
return Err(ApiError::InvalidId(id));
}
if id == "missing" {
return Err(ApiError::NotFound);
}
Ok(Json(serde_json::json!({ "id": id })))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/items/:id", get(get_item));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}O que isso demonstra:
- Handlers
Result<T, ApiError>com conversão automática de erros. thiserrorpara variantes legíveis e#[from]para erros sqlx.- Campo
codeestável separado damessagehumana. - Erros internos ocultam detalhes do banco de dados dos clientes.
Mergulho Profundo
Como Funciona
- Axum implementa
IntoResponseparaResult<T, E>quando tantoTquantoEimplementamIntoResponse. - O operador
?propaga erros de funções auxiliares que retornamResult<_, ApiError>. - Mapeie erros uma vez em
IntoResponse, não em cada handler.
Estratégia de Erro
| Camada | Responsabilidade |
|---|---|
| Domínio | Retornar Result com erros tipados |
| Handler | Usar ? para propagar |
IntoResponse | Mapear para status + JSON |
| Middleware | Capturar panics, adicionar IDs de requisição |
Notas de Rust
// Axum 0.8: mapear tipos de erro com From
impl From<validator::ValidationErrors> for ApiError {
fn from(e: validator::ValidationErrors) -> Self {
ApiError::BadRequest(e.to_string())
}
}Armadilhas
- Vazamento de detalhes internos - Retornar texto de
sqlx::Errorpara clientes expõe detalhes do esquema. Correção: Registrar erro completo; retornar mensagem genérica. - Formas JSON inconsistentes - Alguns erros retornam texto simples. Correção: Uma struct
ErrorBodypara todas as respostas. - Status incorreto para validação - Usar 400 para falhas de serde quando 422 é convencional. Correção: Seguir o padrão da API da equipe; documentar no OpenAPI.
- Panic em unwrap -
unwrap()em handlers se torna 500 com queda de conexão. Correção: Substituir por?e erros tipados. IntoResponseausente para rejeições - Extratores personalizados precisam de tipos de rejeição. Correção: ImplementarIntoResponseem enums de rejeição.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
anyhow em binários | CLIs e scripts internos | APIs HTTP públicas que precisam de códigos estáveis |
Problem Details (RFC 7807) | APIs compatíveis com padrões | Ferramentas internas simples |
axum::response::Response sempre | Flexibilidade máxima | Você quer respostas de sucesso tipadas |
FAQs
Result vs match manual?
Prefira Result com ? - menos boilerplate e caminho de erro consistente.
Onde registrar erros?
Registrar em IntoResponse ou em um middleware dedicado com o ID da requisição.
Múltiplos enums de erro?
Consolidar em um único ApiError com variantes ou usar Box<dyn Error> apenas nas fronteiras.
404 vs 400?
404 quando o ID do recurso não existe; 400 quando o formato do ID é inválido.
Erros de validação?
Mapear erros de validator ou garde para 422 com caminhos de campo.
Integração com tracing?
tracing::error!(?err) em IntoResponse para variantes 5xx.
Testar respostas de erro?
Requisição oneshot e assertar status + campo code do JSON.
Fallback para erros desconhecidos?
Variante catch-all Internal mapeando para 500.
Erros OAuth?
Usar 401 para autenticação ausente, 403 para escopo insuficiente.
Erros de stream?
SSE e WebSocket precisam de sinalização de erro separada no stream.
Relacionados
- Axum Routing & Handlers - Assinaturas de Handler
- Validação de Requisição - Falhas de validação
- Extractors & State - Rejeições de Extrator
- Boas Práticas de Web Backends - Regras de estratégia de erro
- Fundamentos de Tratamento de Erros - Fundamentos de erro em Rust
Versões da Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.