Exceções: throw, try, catch, finally

1. Fundamentos do Tratamento de Exceções em PHP

Exceções são mecanismos para lidar com situações excepcionais durante a execução de um programa. Diferente dos erros tradicionais do PHP (como E_WARNING ou E_NOTICE), as exceções interrompem o fluxo normal do código e podem ser capturadas e tratadas de forma estruturada.

A classe base para exceções é \Exception, que implementa a interface \Throwable (introduzida no PHP 7). A interface \Throwable é a base para todos os objetos que podem ser lançados com throw, incluindo erros do PHP (\Error).

O fluxo básico do tratamento de exceções segue três etapas:
1. Lançar (throw) — quando uma condição excepcional é detectada
2. Capturar (catch) — onde a exceção é tratada
3. Finalizar (finally) — bloco opcional que sempre executa

<?php
try {
    // Código que pode lançar exceção
    if (!file_exists('config.php')) {
        throw new Exception('Arquivo de configuração não encontrado');
    }
} catch (Exception $e) {
    echo 'Erro: ' . $e->getMessage();
}

2. Lançando Exceções com throw

A sintaxe do throw é simples: você pode lançar qualquer objeto que implemente a interface \Throwable. Na prática, isso significa instâncias de \Exception ou suas subclasses.

<?php
function dividir($a, $b) {
    if ($b == 0) {
        throw new Exception('Divisão por zero não permitida', 1001);
    }
    return $a / $b;
}

Boas práticas ao lançar exceções:
- Use mensagens descritivas que ajudem na depuração
- Utilize códigos de erro para categorizar exceções
- Prefira exceções específicas em vez de genéricas

<?php
class DivisaoPorZeroException extends Exception {}

function dividir($a, $b) {
    if ($b == 0) {
        throw new DivisaoPorZeroException(
            'Tentativa de divisão de ' . $a . ' por zero',
            1001
        );
    }
    return $a / $b;
}

3. Bloco try e catch: Capturando Exceções

A estrutura try + catch permite capturar e tratar exceções de forma controlada. Você pode ter múltiplos blocos catch para diferentes tipos de exceção.

<?php
try {
    $arquivo = fopen('dados.txt', 'r');
    if (!$arquivo) {
        throw new Exception('Não foi possível abrir o arquivo');
    }
    $conteudo = fread($arquivo, filesize('dados.txt'));
    fclose($arquivo);
} catch (Exception $e) {
    echo 'Erro: ' . $e->getMessage() . "\n";
    echo 'Código: ' . $e->getCode() . "\n";
    echo 'Arquivo: ' . $e->getFile() . "\n";
    echo 'Linha: ' . $e->getLine() . "\n";
}

A ordem de captura é crucial: sempre capture as exceções mais específicas primeiro.

<?php
try {
    // código que pode lançar diferentes exceções
} catch (DivisaoPorZeroException $e) {
    // Mais específico primeiro
    echo 'Erro de divisão por zero';
} catch (InvalidArgumentException $e) {
    // Depois o menos específico
    echo 'Argumento inválido';
} catch (Exception $e) {
    // Por último, o genérico
    echo 'Erro genérico: ' . $e->getMessage();
}

Os métodos mais úteis do objeto exceção:
- getMessage() — retorna a mensagem da exceção
- getCode() — retorna o código numérico
- getFile() — retorna o arquivo onde a exceção foi lançada
- getLine() — retorna a linha onde a exceção foi lançada
- getTrace() — retorna o stack trace completo

4. Bloco finally: Execução Garantida

O bloco finally é executado sempre, independentemente de uma exceção ter sido lançada ou não. É ideal para liberação de recursos.

<?php
function processarArquivo($caminho) {
    $arquivo = null;
    try {
        $arquivo = fopen($caminho, 'r');
        if (!$arquivo) {
            throw new Exception('Falha ao abrir arquivo');
        }
        // Processa o arquivo...
        return fread($arquivo, 1024);
    } catch (Exception $e) {
        echo 'Erro: ' . $e->getMessage();
        return null;
    } finally {
        if ($arquivo) {
            fclose($arquivo);
            echo 'Arquivo fechado com sucesso';
        }
    }
}

Importante: se houver um return dentro do try ou catch, o finally ainda será executado antes do retorno. Se o finally também tiver um return, ele sobrescreverá o retorno anterior.

5. Hierarquia de Exceções e Exceções Customizadas

Criar exceções personalizadas permite uma captura mais granular e semântica.

<?php
class ValidationException extends Exception {}
class DatabaseException extends Exception {}
class AuthenticationException extends Exception {}

class UsuarioService {
    public function salvar($dados) {
        if (empty($dados['email'])) {
            throw new ValidationException('Email é obrigatório', 400);
        }

        try {
            // Tenta salvar no banco
            $this->db->insert('usuarios', $dados);
        } catch (PDOException $e) {
            throw new DatabaseException(
                'Falha ao salvar usuário: ' . $e->getMessage(),
                500,
                $e
            );
        }
    }
}

// Uso com captura específica
try {
    $service = new UsuarioService();
    $service->salvar($_POST);
} catch (ValidationException $e) {
    http_response_code(400);
    echo json_encode(['erro' => $e->getMessage()]);
} catch (DatabaseException $e) {
    http_response_code(500);
    error_log($e->getMessage());
    echo 'Erro interno do servidor';
}

6. Exceções Não Capturadas e o Manipulador Global

Quando uma exceção não é capturada, o PHP gera um erro fatal. Você pode registrar um manipulador global com set_exception_handler().

<?php
function manipuladorGlobal($exception) {
    http_response_code(500);
    echo json_encode([
        'erro' => 'Erro interno do servidor',
        'mensagem' => $exception->getMessage(),
        'arquivo' => $exception->getFile(),
        'linha' => $exception->getLine()
    ]);
    error_log(
        'Exceção não capturada: ' . $exception->getMessage() .
        ' em ' . $exception->getFile() . ':' . $exception->getLine()
    );
}

set_exception_handler('manipuladorGlobal');

// Esta exceção será capturada pelo manipulador global
throw new Exception('Exemplo de exceção não capturada');

Diferença importante: set_exception_handler() lida com exceções não capturadas, enquanto set_error_handler() lida com erros tradicionais do PHP (como warnings e notices).

7. Boas Práticas e Armadilhas Comuns

Evite capturar exceções genéricas sem necessidade:

<?php
// Ruim - captura genérica demais
try {
    // código
} catch (Exception $e) {
    // Trata tudo igual
}

// Bom - captura específica
try {
    // código
} catch (InvalidArgumentException $e) {
    // Trata apenas argumentos inválidos
}

Relançamento correto de exceções:

<?php
try {
    // código
} catch (Exception $e) {
    // Correto: preserva o stack trace original
    throw $e;

    // Incorreto: perde o stack trace original
    // throw new Exception($e->getMessage());
}

Não use exceções para controle de fluxo normal:

<?php
// Ruim - exceção para fluxo normal
function buscarUsuario($id) {
    if ($id <= 0) {
        throw new Exception('ID inválido');
    }
    // ...
}

// Bom - validação antes de lançar exceção
function buscarUsuario($id) {
    if ($id <= 0) {
        return null; // ou lançar exceção apenas se for realmente excepcional
    }
    // ...
}

Sempre faça logging adequado:

<?php
try {
    // código crítico
} catch (DatabaseException $e) {
    error_log('Falha no banco: ' . $e->getMessage());
    // Apenas mostra mensagem amigável ao usuário
    echo 'Ocorreu um erro. Tente novamente mais tarde.';
}

O tratamento de exceções bem implementado torna seu código mais robusto, previsível e fácil de depurar. Use exceções para situações verdadeiramente excepcionais e sempre forneça mensagens claras e ações adequadas para cada tipo de erro.

Referências