VapVup👤
← Voltar
⚠️

Exceptions em Java

Exceptions são o mecanismo do Java para lidar com situações anormais durante a execução. Entender a hierarquia, saber quando usar checked vs unchecked e dominar try-with-resources é o que separa código robusto de código que explode em produção às 3h da manhã.

🚨Analogia

Exceções são Alarmes, não Extintor

Pense em exceções como alarmes de incêndio em um prédio. O alarme (exceção) avisa que algo deu errado. O protocolo de evacuação (catch) define o que fazer. O zeladoria verificar se tudo está ok depois (finally) sempre acontece. Mas assim como um alarme, se você simplesmente desligar e ignorar, o prédio ainda está pegando fogo — isso é o que acontece quando você faz catch vazio.

💡Conceito

A Hierarquia: Throwable → Exception / Error

Tudo começa em Throwable. Dela derivam dois ramos: Exception (problemas que sua aplicação pode tratar) e Error (problemas da JVM que você não deveria tentar tratar). Exception se divide em checked (o compilador te obriga a tratar) e unchecked (RuntimeException e subclasses, o compilador não exige tratamento). Essa divisão é uma decisão de design da linguagem Java que gera debates até hoje.

📝Analogia

Checked vs Unchecked: Contrato vs Gentileza

Checked exception é como um contrato assinado: o compilador exige que você lide com ela, assim como um contrato exige sua assinatura. IOException diz: 'eu POSSO falhar, e você DEVE ter um plano B'. Unchecked exception é como um buraco na calçada: ninguém te obriga a olhar pro chão, mas se você pisar no buraco (NullPointerException), a culpa é sua. O conserto é no código, não no tratamento.

🔧Exemplo

Checked Exception em Ação

O compilador não deixa esse código compilar sem tratamento. IOException é checked — você DEVE decidir o que fazer com ela:

// Opção 1: tratar com try/catch
public String lerConfig() {
    try {
        return Files.readString(Path.of("config.yml"));
    } catch (IOException e) {
        logger.error("Falha ao ler config", e);
        throw new ConfigException("Config inacessível", e);
    }
}

// Opção 2: propagar com throws
public String lerConfig() throws IOException {
    return Files.readString(Path.of("config.yml"));
}
💡Conceito

Errors: Quando a JVM Grita Socorro

OutOfMemoryError, StackOverflowError, InternalError — esses são problemas da própria JVM. Capturar um OutOfMemoryError é como tentar apagar um incêndio florestal com um copo d'água: mesmo que o catch execute, a JVM provavelmente está em estado inconsistente e não conseguirá alocar memória para fazer nada útil. A solução é prevenção: tunar heap size, evitar memory leaks, não fazer recursão infinita.

🔧Exemplo

try/catch/finally e Multi-catch

O multi-catch (Java 7+) permite tratar múltiplas exceções no mesmo bloco. A variável é implicitamente final. O finally sempre executa — use para cleanup que deve acontecer independente de sucesso ou falha:

try {
    var conn = DriverManager.getConnection(url);
    var stmt = conn.prepareStatement(sql);
    return stmt.executeQuery();
} catch (SQLException | IOException e) {
    // multi-catch: 'e' é final implicitamente
    logger.error("Falha na operação: {}", e.getMessage(), e);
    throw new DataAccessException("Erro de acesso", e);
} finally {
    // Sempre executa! Mesmo com return ou exceção
    metricsCollector.recordAttempt();
}
🚗Analogia

try-with-resources: O Carro que Desliga Sozinho

Antes do Java 7, gerenciar recursos era como ter um carro que você precisa lembrar de desligar manualmente — esqueceu o close(), vazou recurso. try-with-resources é como um carro com auto-stop: quando você sai (sai do bloco try), ele desliga sozinho. E se o motor falhar ao desligar (exceção no close()), essa falha fica registrada como suppressed, sem esconder o problema original.

🔧Exemplo

try-with-resources e Suppressed Exceptions

Qualquer classe que implemente AutoCloseable pode ser usada com try-with-resources. Múltiplos recursos são fechados na ordem inversa da declaração. Se exceções ocorrerem tanto no try quanto no close(), a do close() é adicionada como suppressed:

// Múltiplos recursos — fechados na ordem inversa
try (var conn = dataSource.getConnection();
     var stmt = conn.prepareStatement(sql);
     var rs = stmt.executeQuery()) {

    while (rs.next()) {
        // processar resultados
    }
} // rs.close(), stmt.close(), conn.close() — automático!

// Acessando suppressed exceptions
try {
    processarComRecursos();
} catch (Exception e) {
    logger.error("Principal: {}", e.getMessage());
    for (Throwable s : e.getSuppressed()) {
        logger.warn("Suppressed: {}", s.getMessage());
    }
}
💡Conceito

throw vs throws e Exceções Customizadas

throw lança uma exceção (dentro do método). throws declara que o método pode lançar (na assinatura). Exceções customizadas devem: (1) ter nomes descritivos que expressem o problema de negócio, (2) estender Exception se o chamador deve obrigatoriamente tratar, ou RuntimeException se é erro de programação, (3) ter construtores com message e cause para preservar o encadeamento de exceções.

🔧Exemplo

Anti-patterns que Destroem sua Noite de Sono

Estes são os erros mais comuns em tratamento de exceções. Cada um deles já causou incidentes em produção que levaram horas para debugar:

// ❌ Anti-pattern 1: Swallowing Exception
try {
    processarPedido(pedido);
} catch (Exception e) {
    // silêncio mortal — ninguém saberá que falhou
}

// ❌ Anti-pattern 2: catch genérico
try {
    calcularDesconto(preco);
} catch (Exception e) {
    // captura NullPointerException, ClassCastException...
    // bugs reais são escondidos!
    return BigDecimal.ZERO;
}

// ❌ Anti-pattern 3: perder a causa original
try {
    chamarApiExterna();
} catch (HttpException e) {
    throw new ServiceException("Falha"); // cadê o 'e'?!
}

// ✅ Correto: específico, logado, com causa
try {
    chamarApiExterna();
} catch (HttpTimeoutException e) {
    logger.error("Timeout chamando API X: {}", e.getMessage(), e);
    throw new IntegracaoException("API X indisponível", e);
}