1. O problema de deixar exceções escaparem
Sem tratamento centralizado, uma exceção não capturada em qualquer endpoint derruba a resposta
com um 500 cujo corpo pode incluir, dependendo da configuração,
detalhes internos do servidor — uma informação valiosa demais para expor a quem consome a API,
e uma experiência ruim para quem só queria saber "algo deu errado".
2. Middleware de tratamento global de exceções
var app = builder.Build();
app.UseExceptionHandler(app =>
{
app.Run(async context =>
{
var feature = context.Features.Get<IExceptionHandlerFeature>();
var excecao = feature?.Error;
var (status, mensagem) = MapearExcecaoParaResposta(excecao);
context.Response.StatusCode = status;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new { erro = mensagem });
});
});
static (int Status, string Mensagem) MapearExcecaoParaResposta(Exception? excecao) => excecao switch
{
SqlException { Number: 50010 } => (StatusCodes.Status409Conflict, "Estoque insuficiente"),
SqlException { Number: 50001 } => (StatusCodes.Status400BadRequest, "Valor de transferência inválido"),
ArgumentException => (StatusCodes.Status400BadRequest, excecao.Message),
InvalidOperationException => (StatusCodes.Status409Conflict, excecao.Message),
_ => (StatusCodes.Status500InternalServerError, "Erro interno do servidor"),
};
Repare o uso do pattern matching de switch expression com propriedade
(SqlException {'{'} Number: 50010 {'}'}) — visto de relance no capítulo
3 do Módulo 1, e agora aplicado exatamente para traduzir os códigos de erro customizados que você
definiu nas procedures do Módulo 2 em respostas HTTP com significado.
Só o default (qualquer coisa não mapeada explicitamente) devolve a
mensagem genérica "Erro interno do servidor" com 500. Toda exceção
de negócio conhecida (estoque insuficiente, validação, conflito) deveria ser mapeada para um
código de status específico com uma mensagem que faça sentido para quem consome a API.
3. Problem Details — o formato padrão de erro em REST
ProblemDetails é um formato padronizado (RFC 7807) para descrever
erros HTTP, já suportado nativamente pelo ASP.NET Core:
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = status,
Title = mensagem,
Type = $"https://httpstatuses.com/{status}",
Instance = context.Request.Path
});
// gera algo como:
// {
// "status": 409,
// "title": "Estoque insuficiente",
// "type": "https://httpstatuses.com/409",
// "instance": "/api/pedidos/checkout"
// }
Quando o projeto usa [ApiController] (capítulo 2), falhas de
validação de ModelState já são devolvidas automaticamente nesse
formato — vale usar o mesmo padrão para os erros que você trata manualmente, para manter a API
consistente.
4. Logando o erro antes de responder
Traduzir a exceção para uma resposta amigável não substitui registrá-la — o padrão do capítulo 6 do Módulo 2 (log em tabela) se aplica aqui também, ou mais comumente, um provedor de log estruturado:
app.UseExceptionHandler(appErro =>
{
appErro.Run(async context =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
var excecao = context.Features.Get<IExceptionHandlerFeature>()?.Error;
logger.LogError(excecao, "Erro não tratado em {Path}", context.Request.Path);
// ... resto do tratamento e resposta
});
});
5. Onde isso te leva
Com erros tratados de forma consistente, o próximo (e penúltimo) capítulo deste módulo cobre boas práticas gerais de design de API — paginação, filtros, e convenções que tornam uma API previsível de usar.