W3docs

Exceções Personalizadas em Java

Defina suas próprias classes de exceção em Java estendendo Exception ou RuntimeException para erros específicos do domínio.

As exceções integradas cobrem a maioria das falhas gerais, mas não sabem nada sobre o seu domínio. Quando algo falha de uma forma específica ao seu código — "usuário não encontrado", "cupom inválido", "configuração fora de sincronia" — a decisão certa geralmente é definir seu próprio tipo de exceção. Exceções personalizadas custam pouco para escrever, tornam os stack traces autoexplicativos e permitem que quem chama trate exatamente a falha que lhe interessa.

A estrutura mínima

Uma exceção personalizada é uma classe que estende Exception (ou uma de suas subclasses). A versão mais curta e útil:

public class UserNotFoundException extends Exception {
  public UserNotFoundException(String message) {
    super(message);
  }
}

Isso é uma exceção personalizada checked completa. Você pode usar throw new UserNotFoundException("id=42") de qualquer lugar, e quem chama pode usar catch (UserNotFoundException e).

Checked ou unchecked?

A decisão mais importante ao definir uma classe de exceção: o que você estende?

  • extends Exception → checked. O compilador obriga quem chama a tratar ou declarar a exceção.
  • extends RuntimeException → unchecked. Quem chama pode tratá-la, mas não é obrigado.

A mesma lógica de exceções checked vs. unchecked se aplica: estenda Exception quando quem chama pode realisticamente se recuperar e você quer forçá-lo a pensar sobre isso; estenda RuntimeException quando a falha representa um bug ou uma condição da qual ninguém pode se recuperar de forma sensata.

Para exceções de domínio em código Java moderno, RuntimeException é a escolha mais comum — em parte porque exceções checked não se compõem bem com streams e lambdas, e em parte porque a maioria das falhas de domínio sobe até um único handler de nível superior. Comece com RuntimeException a menos que tenha uma razão específica para forçar o tratamento.

Os quatro construtores

Por convenção, uma classe de exceção fornece os mesmos quatro construtores que os tipos integrados:

public class ConfigLoadException extends RuntimeException {
  public ConfigLoadException() {
    super();
  }
  public ConfigLoadException(String message) {
    super(message);
  }
  public ConfigLoadException(String message, Throwable cause) {
    super(message, cause);
  }
  public ConfigLoadException(Throwable cause) {
    super(cause);
  }
}

Por que todos os quatro:

  • Sem argumentos — para ferramentas e frameworks que usam reflexão na classe.
  • Somente mensagem — o caso comum no seu próprio código.
  • Mensagem + causa — para encapsular uma exceção de nível inferior. O mais importante a incluir.
  • Somente causa — para quando a mensagem da causa já é descritiva.

Você não precisa digitar todos os quatro toda vez — IDEs os geram com um atalho de teclado — mas omitir as formas com causa é uma perda real. Sem elas, você não consegue preservar a exceção subjacente ao encapsular.

Carregando estado útil

Strings são aceitáveis, mas campos personalizados são melhores. Se quem chama pode querer saber qual usuário não foi encontrado, exponha isso:

public class UserNotFoundException extends RuntimeException {
  private final String userId;

  public UserNotFoundException(String userId) {
    super("user not found: " + userId);
    this.userId = userId;
  }

  public String getUserId() { return userId; }
}

Agora um bloco catch pode fazer algo com a falha em vez de apenas analisar a mensagem:

catch (UserNotFoundException e) {
  metrics.recordMissingUser(e.getUserId());
  return Response.notFound();
}

Mantenha os campos imutáveis (final) e o construtor mínimo. Exceções são construídas no caminho de falha — devem ser rápidas e nunca lançar exceções elas mesmas.

Encapsulando com uma causa

A técnica mais útil com exceções personalizadas é traduzir uma exceção de baixo nível em uma do domínio, preservando a original:

public Config load(Path p) {
  try {
    return parser.parse(Files.readString(p));
  } catch (IOException e) {
    throw new ConfigLoadException("could not read " + p, e);
  } catch (ParseException e) {
    throw new ConfigLoadException("invalid config in " + p, e);
  }
}

Quem chama vê um único tipo de exceção que corresponde ao vocabulário da sua camada. A falha original não é perdida — ela está pendurada em getCause() e aparece no printStackTrace() sob uma linha Caused by:.

É assim que você mantém as camadas separadas. A API Config não vaza IOException nem ParseException; ambas são traduzidas em algo que significa "falha ao carregar a configuração."

Uma pequena hierarquia

Quando você tem uma família de falhas relacionadas, dê a elas um pai compartilhado:

public class PaymentException extends RuntimeException {
  public PaymentException(String message)               { super(message); }
  public PaymentException(String message, Throwable c)  { super(message, c); }
}

public class CardDeclinedException extends PaymentException {
  public CardDeclinedException(String message) { super(message); }
}
public class InsufficientFundsException extends PaymentException {
  public InsufficientFundsException(String message) { super(message); }
}
public class FraudCheckFailedException extends PaymentException {
  public FraudCheckFailedException(String message) { super(message); }
}

Quem chama pode ser específico (catch (CardDeclinedException)) ou abrangente (catch (PaymentException)) conforme necessário. Um pai compartilhado também oferece uma única importação para usar em uma cláusula throws quando o método pode lançar qualquer uma delas.

O que evitar

  • Não estenda Throwable ou Error diretamente. Sempre passe por Exception ou RuntimeException.
  • Não sobrescreva getMessage() para calcular strings a cada chamada. Construa a mensagem no construtor e deixe a classe pai armazená-la.
  • Não coloque lógica na exceção. Ela existe para carregar informações. A recuperação pertence ao catch.
  • Não prolifere. Cada novo tipo de exceção é um pequeno contrato que quem chama pode querer tratar. Se duas falhas realmente querem tratamento idêntico, provavelmente deveriam ser o mesmo tipo.

Um exemplo completo

Um pequeno módulo de processamento de pedidos com sua própria família de exceções. A classe base encapsula falhas de nível inferior; as subclasses carregam detalhes do domínio; o driver captura-as em diferentes níveis de especificidade para mostrar como a hierarquia permite que você escolha.

java— editable, runs on the server

O driver captura EmptyOrderException primeiro (o caso específico que quer tratar de forma diferente), depois OrderException como catch-all para a família. Quando a validação falha, a cadeia de causas remete à IllegalStateException original, para que você não perca informações ao traduzir para o tipo de domínio.

O que vem a seguir

Agora você tem toda a mecânica. O capítulo final aborda o lado do julgamento — quando lançar, quando capturar, o que registrar e os padrões que distinguem um código de tratamento de exceções maduro do ruído defensivo. Continue para boas práticas de tratamento de exceções em Java.

Prática

Prática
Você está projetando uma exceção para 'pagamento recusado pela operadora do cartão'. Quem chama pode querer tentar novamente, informar o usuário ou usar outro cartão. Qual é o design mais defensável?
Você está projetando uma exceção para 'pagamento recusado pela operadora do cartão'. Quem chama pode querer tentar novamente, informar o usuário ou usar outro cartão. Qual é o design mais defensável?
Was this page helpful?