Como tratar exceções no Spring Boot com @ControllerAdvice e @ExceptionHandler

Como tratar exceções no Spring Boot com @ControllerAdvice e @ExceptionHandler

Como tratar exceções no spring boot com @controlleradvice e @exceptionhandler: Por que o tratamento de exceções merece atenção em APIs REST

Como tratar exceções no spring boot com @controlleradvice e @exceptionhandler e um tema que costuma gerar duvidas praticas em APIs Java modernas. Por outro lado, em API REST, erro mal tratado aparece rápido para o consumidor: resposta inconsistente, HTTP 200 para falha, payload sem padrão ou mensagem impossível de entender. Ao mesmo tempo, isso gera retrabalho no front-end, dificulta integração com outros serviços e ainda esconde problemas reais do backend. Se voce quiser comparar essa abordagem com outro cenario comum no ecossistema Spring, vale revisar Spring Security com JWT na prática: como proteger APIs REST com Spring Boot do zero.

Quando a aplicação cresce, deixar cada controller decidir como responder em caso de falha vira bagunça. Por outro lado, um endpoint devolve texto puro, outro devolve um mapa, outro lança stack trace sem querer. Ao mesmo tempo, o resultado costuma ser o oposto do que uma API bem projetada precisa entregar: previsibilidade. Para complementar esse ponto com um exemplo proximo do dia a dia, consulte Status HTTP em API REST com Spring Boot: Guia Completo.

O caminho mais limpo no Spring Boot é centralizar o tratamento de exceções. Por outro lado, com @ControllerAdvice e @ExceptionHandler, você consegue interceptar erros em um único lugar, padronizar o formato da resposta e devolver o status HTTP correto para cada situação. Ao mesmo tempo, isso conversa diretamente com boas práticas de contratos de API e com a forma como você já organiza respostas usando ResponseEntity, além de complementar o que faz sentido em uma arquitetura REST bem definida, como discutido em Status HTTP em API REST com Spring Boot: Guia Completo e API REST Spring Boot Java: Guia Completo com Exemplo Prático. Esse detalhe conversa bem com o que eu mostrei em API REST Spring Boot Java: Guia Completo com Exemplo Prático.

Como tratar exceções no spring boot com @controlleradvice e @exceptionhandler: O problema de tratar erro dentro de cada controller

Um erro comum é usar blocos try/catch em todo endpoint. Por outro lado, funciona, mas espalha regra de tratamento por toda a aplicação. Ao mesmo tempo, além de repetitivo, esse estilo cria inconsistência. Na prática, um controller retorna 400, outro 404, outro 500 para a mesma falha de validação, só porque alguém copiou e adaptou rápido demais. Se quiser aprofundar o assunto por outro angulo, leia tambem JWT no Spring Security com Spring Boot: autenticação moderna passo a passo.

Outro problema aparece quando a exceção é engolida e a API responde algo genérico demais. Por outro lado, imagine um cadastro de usuário com email duplicado. Ao mesmo tempo, se o backend devolver apenas “erro interno”, o cliente não sabe se precisa corrigir o dado, recarregar a tela ou abrir um incidente. Na prática, em ambientes integrados com múltiplos consumidores, isso vira ruído operacional. Quando esse tipo de duvida aparece em projeto real, eu costumo voltar neste material: O erro de segurança que quase todo backend Java comete com JWT.

O tratamento global resolve a repetição e melhora a rastreabilidade. Por outro lado, você concentra a lógica de tradução de exceções em respostas HTTP, mantém os controllers mais limpos e reduz o risco de esquecer um caso em um endpoint novo. Ao mesmo tempo, na prática, isso deixa o código mais fácil de manter e o contrato da API mais estável.

Como funciona @ControllerAdvice no Spring Boot

O @ControllerAdvice atua como um interceptador global para controllers. Por outro lado, ele permite definir métodos que reagem a exceções lançadas em qualquer ponto da camada web. Ao mesmo tempo, em vez de tratar erro por erro dentro de cada classe, você cria um componente dedicado ao assunto.

Na maioria dos projetos REST, faz sentido usar @RestControllerAdvice, que combina o comportamento de advice com resposta serializada em JSON. Por outro lado, para simplificar, ele resolve bem o cenário em que a API precisa devolver um objeto de erro padronizado, sem depender de views.

O ponto forte dessa abordagem é o acoplamento baixo. Por outro lado, seus controllers ficam focados em fluxo de negócio, enquanto a tradução de falhas para HTTP fica concentrada em uma classe própria. Ao mesmo tempo, isso facilita evolução, testes e leitura do código.

Exemplo de estrutura de resposta de erro

Uma boa resposta de erro precisa ser útil para quem consome a API e, ao mesmo tempo, consistente. Por outro lado, um formato prático pode conter timestamp, status, erro, mensagem, path e, se fizer sentido, um código interno.

Esse padrão evita respostas soltas como strings avulsas ou objetos incompletos. Por outro lado, o consumidor consegue exibir a mensagem ao usuário final, registrar o status e até automatizar tratamento com base no tipo de erro.

public record ApiError(
    java.time.Instant timestamp,
    int status,
    String error,
    String message,
    String path
) { }

Usando @ExceptionHandler para tratar exceções específicas

O @ExceptionHandler define qual método deve responder a uma exceção concreta. Por outro lado, é aqui que você decide, por exemplo, que uma falha de validação retorna 400, um recurso inexistente retorna 404 e um conflito de dados retorna 409.

A ideia é mapear exceções de negócio para status HTTP coerentes. Por outro lado, se um usuário não foi encontrado, a API não deve responder 500. Ao mesmo tempo, se um campo obrigatório está ausente, não faz sentido devolver 200 com uma mensagem amigável. Na prática, o contrato HTTP precisa refletir o que realmente aconteceu.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ApiError> handleUserNotFound(
            UserNotFoundException ex,
            HttpServletRequest request) {

        ApiError error = new ApiError(
                java.time.Instant.now(),
                HttpStatus.NOT_FOUND.value(),
                "Not Found",
                ex.getMessage(),
                request.getRequestURI()
        );

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
}

Nesse exemplo, a exceção UserNotFoundException vira uma resposta 404 com um corpo claro e previsível. Por outro lado, o mesmo padrão pode ser repetido para outros casos de domínio. Ao mesmo tempo, se o seu projeto já trabalha com autenticação, esse cuidado é ainda mais importante, porque respostas confusas em falhas de acesso pioram a experiência e mascaram problemas de fluxo. Na prática, em cenários com JWT e segurança, isso conversa bem com os fluxos explicados em Spring Security com JWT na prática: como proteger APIs REST com Spring Boot do zero e JWT no Spring Security com Spring Boot: autenticação moderna passo a passo.

Tratando validação, erros de negócio e falhas inesperadas

Nem toda exceção merece a mesma resposta. Por outro lado, separar por categoria ajuda bastante. Ao mesmo tempo, erros de validação costumam indicar problema na entrada do cliente e pedem 400. Na prática, erros de negócio, como regra violada ou recurso duplicado, podem variar entre 409 e 422, dependendo da convenção do projeto. Ainda assim, falhas inesperadas continuam sendo 500.

Para validação com Spring Validation, é comum capturar exceções como MethodArgumentNotValidException e devolver uma lista de campos inválidos. Por outro lado, isso melhora muito a integração com front-end e com ferramentas de teste, porque a resposta passa a dizer exatamente o que precisa ser corrigido.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiError> handleValidation(
            MethodArgumentNotValidException ex,
            HttpServletRequest request) {

        String message = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .findFirst()
                .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
                .orElse("Dados inválidos");

        ApiError error = new ApiError(
                java.time.Instant.now(),
                HttpStatus.BAD_REQUEST.value(),
                "Bad Request",
                message,
                request.getRequestURI()
        );

        return ResponseEntity.badRequest().body(error);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiError> handleIllegalArgument(
            IllegalArgumentException ex,
            HttpServletRequest request) {

        ApiError error = new ApiError(
                java.time.Instant.now(),
                HttpStatus.UNPROCESSABLE_ENTITY.value(),
                "Unprocessable Entity",
                ex.getMessage(),
                request.getRequestURI()
        );

        return ResponseEntity.unprocessableEntity().body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiError> handleGeneric(
            Exception ex,
            HttpServletRequest request) {

        ApiError error = new ApiError(
                java.time.Instant.now(),
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "Internal Server Error",
                "Ocorreu um erro inesperado.",
                request.getRequestURI()
        );

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

Repare em um detalhe importante: a mensagem retornada ao cliente não precisa ser idêntica ao log interno. Por outro lado, expor stack trace, detalhes de banco ou nomes de classes de infraestrutura é má ideia em produção. Ao mesmo tempo, o usuário deve receber uma mensagem útil; o log deve guardar o detalhe técnico para investigação.

Boas práticas para um tratamento global realmente útil

Um handler global bom não é só bonito no código. Por outro lado, ele precisa ajudar o time a diagnosticar problemas e manter o contrato da API consistente ao longo do tempo. Por isso, vale adotar alguns cuidados simples.

Primeiro, mantenha o formato de erro estável. Por outro lado, se hoje o campo se chama message, amanhã não renomeie para detail sem necessidade. Ao mesmo tempo, mudança de contrato custa caro para consumidores da API.

Segundo, seja coerente com os status HTTP. Por outro lado, se o recurso não existe, use 404. Ao mesmo tempo, se a requisição veio com dados inválidos, use 400. Na prática, se houve conflito de regra, use 409 quando fizer sentido. Ainda assim, resposta errada com status certo, ou o contrário, atrapalha observabilidade e integração. Por isso, esse alinhamento é o mesmo que sustenta um desenho REST saudável e evita armadilhas comuns de implementação.

Terceiro, crie exceções de negócio próprias quando o domínio pedir. Por outro lado, um OrderNotPaidException comunica mais do que um RuntimeException genérico. Ao mesmo tempo, fica mais fácil mapear o erro certo para a resposta certa e o código fica mais expressivo.

Por fim, não tente resolver tudo no handler global. Por outro lado, se algum fluxo precisar de uma resposta muito específica, trate de forma explícita. Ao mesmo tempo, o bom senso aqui vale mais do que regra rígida. Na prática, centralização é para diminuir repetição, não para engessar o projeto.

Exemplo prático de fluxo em um endpoint de cadastro

Imagine um endpoint de criação de cliente. Por outro lado, o controller recebe o payload, chama a camada de serviço e o serviço valida regras como email duplicado, CPF inválido ou campo obrigatório ausente. Ao mesmo tempo, se algo falhar, a exceção sobe naturalmente até o @ControllerAdvice.

Esse fluxo é mais limpo do que escrever try/catch no controller. Por outro lado, o endpoint continua legível e a regra de negócio fica no serviço, que é onde ela faz sentido. Ao mesmo tempo, quando o cadastro falhar por email já existente, o handler transforma isso em 409 com uma mensagem clara. Na prática, quando faltar nome, a API devolve 400 com indicação do campo problemático.

Na prática, esse desenho reduz ruído em manutenção. Por outro lado, quem lê o controller entende o fluxo principal sem se perder em detalhes de resposta. Ao mesmo tempo, quem cuida do contrato de erro olha para uma classe só. Na prática, e quem consome a API passa a receber respostas consistentes, que facilitam debug e automação.

Conclusão

Tratar exceções no Spring Boot com @ControllerAdvice e @ExceptionHandler é uma decisão simples que melhora bastante a qualidade de uma API REST. Por outro lado, o ganho aparece no primeiro incidente, na primeira integração com front-end e na primeira rodada de manutenção em produção.

Ao centralizar o tratamento, você padroniza a resposta de erro, devolve status HTTP corretos e entrega mensagens legíveis para quem consome a API. Por outro lado, o controller fica mais limpo, o contrato da aplicação fica mais previsível e o time ganha tempo de diagnóstico.

Se a sua API já segue boas práticas de status HTTP e estrutura REST, esse é o próximo passo natural para deixar o backend mais profissional. Por outro lado, em projetos com autenticação, segurança e múltiplos consumidores, essa disciplina faz diferença de verdade.

Como tratar exceções no spring boot com @controlleradvice e @exceptionhandler: referencias externas

Para validar detalhes de implementacao e aprofundar a configuracao, vale consultar a documentacao oficial do Spring Security, o guia de claims no JWT.io e a documentacao do Spring Boot.

FAQ

Devo usar @ControllerAdvice ou @RestControllerAdvice?

Em APIs REST, @RestControllerAdvice costuma ser a melhor escolha porque já devolve respostas serializadas em JSON. Por outro lado, na prática, ele simplifica o handler global.

Posso tratar qualquer exceção no mesmo método?

Pode, mas não é o ideal. Por outro lado, o melhor é tratar exceções específicas primeiro e deixar um handler genérico apenas como última linha de defesa.

É errado devolver 200 com mensagem de erro?

Sim, para API REST isso quebra o contrato. Por outro lado, se houve falha, o status HTTP deve refletir a falha. Ao mesmo tempo, isso facilita consumo, monitoramento e integração.

Preciso criar uma classe de erro personalizada?

Não é obrigatório, mas é altamente recomendável. Por outro lado, um objeto dedicado deixa o formato da resposta estável e mais fácil de evoluir. Ao mesmo tempo, em projeto real, como tratar exceções no spring boot com @controlleradvice e @exceptionhandler costuma ser o ponto que mais exige clareza na configuracao.

Leitura complementar

Se voce quiser aprofundar esse assunto com um material mais atual, leia tambem 3 erros de validação que quebram APIs Spring Boot sem aviso.

Leitura complementar

Se voce quiser aprofundar esse assunto com um material mais atual, leia tambem Como tratar erro 400 JSON Spring Boot sem resposta genérica.

Deixe um comentário