Exceções são mecanismos poderosos para lidar com erros em tempo de execução de forma controlada. Em PHP, o tratamento de exceções permite que você capture e responda a condições excepcionais sem interromper abruptamente o fluxo do programa. Nesta aula, exploraremos como usar try/catch/finally, como lançar exceções com throw, a hierarquia de classes de exceção e as melhores práticas para escrever código resiliente.

Dominar exceções é essencial para criar aplicações robustas que lidam graciosamente com falhas de banco de dados, arquivos ausentes, entradas inválidas e outros imprevistos. Vamos mergulhar nos detalhes.

try/catch/finally

O bloco try envolve o código que pode lançar uma exceção. Se uma exceção for lançada dentro do bloco try, a execução é transferida para o bloco catch correspondente, onde você pode tratar o erro. O bloco finally é executado sempre, independentemente de uma exceção ter sido lançada ou não, sendo útil para liberar recursos como conexões de banco de dados ou arquivos abertos.

Exemplo básico:

try {
    $file = fopen("arquivo.txt", "r");
    if (!$file) {
        throw new Exception("Não foi possível abrir o arquivo.");
    }
    // leitura do arquivo...
} catch (Exception $e) {
    echo "Erro: " . $e->getMessage();
} finally {
    if (isset($file) && $file) {
        fclose($file);
    }
}

Você pode ter múltiplos blocos catch para tratar diferentes tipos de exceção, e o PHP executará o primeiro que corresponder à exceção lançada. O finally é opcional, mas muito útil para limpeza.

throw

A instrução throw é usada para lançar uma exceção. Você pode lançar qualquer objeto que implemente a interface Throwable, geralmente uma instância de Exception ou suas subclasses. O throw interrompe imediatamente a execução da função atual e retorna ao bloco catch mais próximo.

Exemplo de lançamento condicional:

function dividir($a, $b) {
    if ($b == 0) {
        throw new InvalidArgumentException("Divisão por zero não permitida.");
    }
    return $a / $b;
}

try {
    echo dividir(10, 0);
} catch (InvalidArgumentException $e) {
    echo "Erro: " . $e->getMessage();
}

É possível relançar uma exceção dentro de um bloco catch usando throw $e;, o que permite que o erro seja tratado em um nível superior.

Hierarquia de exceções

O PHP possui uma hierarquia de classes de exceção encabeçada pela interface Throwable, que é implementada por Exception e Error. A partir do PHP 7, erros fatais (como erros de tipo) são lançados como Error, que também pode ser capturado. Abaixo de Exception, existem várias subclasses nativas, como InvalidArgumentException, RuntimeException, OutOfBoundsException, etc.

Você pode criar suas próprias classes de exceção estendendo Exception ou qualquer outra classe da hierarquia:

class MinhaExcecao extends Exception {}

try {
    throw new MinhaExcecao("Erro personalizado.");
} catch (MinhaExcecao $e) {
    echo "Capturado: " . $e->getMessage();
}

Entender a hierarquia ajuda a capturar exceções no nível adequado de especificidade, evitando capturas genéricas demais ou específicas demais.

Boas práticas

  • Seja específico: Capture exceções específicas em vez de usar catch (Exception $e) genérico, a menos que seja realmente necessário.
  • Não use exceções para controle de fluxo normal: Exceções são para situações excepcionais, não para substituir estruturas condicionais.
  • Documente as exceções: Use docblocks para indicar quais exceções uma função pode lançar, facilitando o uso por outros desenvolvedores.
  • Libere recursos no finally: Sempre feche arquivos, conexões de banco de dados ou outros recursos no bloco finally para evitar vazamentos.
  • Não silencie exceções: Evite blocos catch vazios ou que apenas registram e continuam sem ação adequada.

Além disso, considere criar exceções personalizadas para diferentes domínios da sua aplicação, como DatabaseException ou ValidationException, para facilitar o tratamento granular.

Referências

Exercícios

  1. Crie uma função que valida uma idade (deve ser inteiro entre 0 e 150). Se a idade for inválida, lance uma InvalidArgumentException com uma mensagem descritiva. Capture a exceção e exiba a mensagem.

    ✓ Resposta:
    function validarIdade($idade) {
        if (!is_int($idade) || $idade < 0 || $idade > 150) {
            throw new InvalidArgumentException("Idade deve ser um inteiro entre 0 e 150.");
        }
        return true;
    }
    
    try {
        validarIdade(200);
    } catch (InvalidArgumentException $e) {
        echo "Erro: " . $e->getMessage();
    }
    
  2. Escreva um código que tenta abrir um arquivo para leitura. Se o arquivo não existir, capture a exceção e crie o arquivo vazio. Use finally para fechar o arquivo se ele foi aberto.

    ✓ Resposta:
    $arquivo = "dados.txt";
    $handle = null;
    try {
        $handle = fopen($arquivo, "r");
        if (!$handle) {
            throw new Exception("Não foi possível abrir o arquivo.");
        }
        // leitura...
    } catch (Exception $e) {
        // Cria o arquivo vazio
        $handle = fopen($arquivo, "w");
        fclose($handle);
        echo "Arquivo criado.";
    } finally {
        if ($handle) {
            fclose($handle);
        }
    }
    
  3. Crie uma classe de exceção personalizada chamada SaldoInsuficienteException que estende Exception. Em uma função sacar($valor), lance essa exceção se o saldo for menor que o valor. Capture-a e exiba uma mensagem.

    ✓ Resposta:
    class SaldoInsuficienteException extends Exception {}
    
    $saldo = 100;
    function sacar($valor) {
        global $saldo;
        if ($valor > $saldo) {
            throw new SaldoInsuficienteException("Saldo insuficiente para saque de R$ $valor.");
        }
        $saldo -= $valor;
    }
    
    try {
        sacar(150);
    } catch (SaldoInsuficienteException $e) {
        echo "Erro: " . $e->getMessage();
    }
    
  4. Implemente uma função que divide dois números e capture DivisionByZeroError (que é um erro, não exceção). Exiba uma mensagem amigável.

    ✓ Resposta:
    function dividir($a, $b) {
        if ($b == 0) {
            throw new DivisionByZeroError("Divisão por zero.");
        }
        return $a / $b;
    }
    
    try {
        echo dividir(10, 0);
    } catch (DivisionByZeroError $e) {
        echo "Erro: Não é possível dividir por zero.";
    }
    
  5. Crie um bloco try/catch que capture tanto InvalidArgumentException quanto RuntimeException em catches separados. Dentro do try, lance uma RuntimeException com uma mensagem.

    ✓ Resposta:
    try {
        throw new RuntimeException("Erro de execução.");
    } catch (InvalidArgumentException $e) {
        echo "InvalidArgumentException: " . $e->getMessage();
    } catch (RuntimeException $e) {
        echo "RuntimeException: " . $e->getMessage();
    }