Tratamento de erros é onde código bom se separa de código profissional. O básico — try/catch/finally — você já domina. Neste artigo avançamos para o design de hierarquias de exceções, o contrato Throwable, encadeamento de causa com $previous, handlers globais com set_exception_handler(), e os antipadrões que transformam exceções de aliadas em pesadelo de manutenção.
Esses conceitos aparecem diretamente em frameworks PHP: o Laravel usa uma hierarquia própria (HttpException, ModelNotFoundException, ValidationException), o Symfony tem HttpExceptionInterface que o kernel reconhece automaticamente, e o tratamento correto de exceções determina se sua API retorna uma mensagem útil ou expõe um stack trace inteiro para o cliente.
A hierarquia Throwable do PHP
No PHP 7+, toda classe lançável implementa a interface Throwable. Ela tem dois ramos: Error (erros internos do PHP) e Exception (exceções de aplicação). A distinção é importante: Error geralmente indica um bug no código, enquanto Exception representa condições previstas e recuperáveis.
<?php
declare(strict_types=1);
// Capturar Exception NÃO captura Error — TypeError é um Error
function somaEstrita(int $a, int $b): int { return $a + $b; }
try {
somaEstrita("dois", 3); // TypeError — string onde int é esperado
} catch (Exception $e) {
// NÃO captura — TypeError é Error, não Exception
} catch (TypeError $e) {
// Captura corretamente
echo "TypeError: " . $e->getMessage() . "\n";
}
// Throwable captura TUDO — use apenas em handlers de último recurso
try {
somaEstrita("dois", 3);
} catch (\Throwable $t) {
// Captura Error E Exception — útil apenas nas bordas do sistema
echo get_class($t) . ": " . $t->getMessage() . "\n";
// TypeError: somaEstrita(): Argument #1 must be of type int, string given
}
// Múltiplos tipos num mesmo catch — PHP 8+
try {
// código que pode lançar tipos diferentes
} catch (InvalidArgumentException | OverflowException $e) {
// trata os dois da mesma forma — sem duplicar código
echo "Erro de validação: " . $e->getMessage();
}
Desenhando sua hierarquia de exceções
Uma hierarquia bem projetada permite que o código cliente capture exceções no nível de granularidade certo. A regra de ouro: crie uma exceção base por módulo/domínio e derive exceções específicas dela. Assim o chamador pode capturar PedidoException para tratar qualquer erro de pedido, ou PedidoNaoEncontradoException para um caso específico.
<?php
declare(strict_types=1);
namespace MeuApp\Exceptions;
// Raiz da hierarquia — toda exceção da aplicação estende esta
abstract class AppException extends \RuntimeException {}
// ── HTTP / API ───────────────────────────────────────────────────────
class HttpException extends AppException
{
public function __construct(
public readonly int $statusHttp,
string $mensagem,
\Throwable $anterior = null,
) {
parent::__construct($mensagem, $statusHttp, $anterior);
}
}
class NaoEncontradoException extends HttpException
{
public function __construct(string $recurso, int|string $id)
{
parent::__construct(404, "{$recurso} com ID '{$id}' não encontrado.");
}
}
class NaoAutorizadoException extends HttpException
{
public function __construct(string $acao = "acessar este recurso")
{
parent::__construct(403, "Você não tem permissão para {$acao}.");
}
}
// ── VALIDAÇÃO ────────────────────────────────────────────────────────
class ValidacaoException extends AppException
{
public function __construct(
/** @var array<string, string[]> */
public readonly array $erros,
) {
parent::__construct("Erro de validação: " . implode("; ", array_merge(...$erros)));
}
public function errosPorCampo(string $campo): array
{
return $this->erros[$campo] ?? [];
}
}
// ── DOMÍNIO: PEDIDOS ─────────────────────────────────────────────────
abstract class PedidoException extends AppException {}
class PedidoNaoEncontradoException extends PedidoException
{
public function __construct(
public readonly int $pedidoId,
\Throwable $anterior = null,
) {
parent::__construct("Pedido #{$pedidoId} não encontrado.", 404, $anterior);
}
}
class EstoqueInsuficienteException extends PedidoException
{
public function __construct(
public readonly string $produto,
public readonly int $solicitado,
public readonly int $disponivel,
) {
parent::__construct(
"Estoque insuficiente para '{$produto}': solicitado {$solicitado}, disponível {$disponivel}."
);
}
}
// Granularidade de captura — do mais específico ao mais genérico
try {
throw new PedidoNaoEncontradoException(999);
} catch (PedidoNaoEncontradoException $e) {
// Mais específico — trata só "pedido não encontrado"
echo "Pedido ID: " . $e->pedidoId . "\n";
} catch (PedidoException $e) {
// Médio — trata qualquer erro de pedido
} catch (AppException $e) {
// Amplo — trata qualquer erro da aplicação
}
Encadeamento de exceções — preservando a causa
Quando você captura uma exceção de baixo nível (erro de PDO, falha de rede) e relança uma exceção de alto nível mais significativa, é fundamental preservar a exceção original como causa usando o parâmetro $previous. Isso mantém o stack trace completo para depuração sem vazar detalhes de implementação para o código cliente.
A regra é clara: o cliente da API vê a mensagem de domínio. O log registra a causa técnica. Nunca inverta isso.
<?php
declare(strict_types=1);
class PedidoRepository
{
public function buscarPorId(int $id): array
{
try {
// Simula uma falha de banco de dados
throw new \PDOException("SQLSTATE[42S02]: Table 'pedidos' doesn't exist");
} catch (\PDOException $pdoException) {
// Traduz exceção de infra → exceção de domínio
// $pdoException como $previous — preserva o contexto completo
throw new PedidoNaoEncontradoException($id, $pdoException);
}
}
}
try {
(new PedidoRepository())->buscarPorId(42);
} catch (PedidoNaoEncontradoException $e) {
echo "Mensagem: " . $e->getMessage() . "\n";
echo "Código: " . $e->getCode() . "\n";
echo "Pedido ID: " . $e->pedidoId . "\n";
// getPrevious() retorna a PDOException original
// Deve ser logada — nunca exposta ao cliente
$causa = $e->getPrevious();
if ($causa) {
error_log("Causa técnica: " . $causa->getMessage());
}
}
// Mensagem: Pedido #42 não encontrado.
// Código: 404
// Pedido ID: 42
Finally — garantindo limpeza de recursos
O bloco finally executa sempre, independente de exceção ser lançada, capturada ou não capturada — inclusive quando há return dentro do try. É o lugar correto para liberar conexões, arquivos e locks. Nunca coloque esse código no try, pois uma exceção impediria sua execução.
<?php
declare(strict_types=1);
function processarArquivo(string $caminho): array
{
$handle = null;
try {
$handle = fopen($caminho, 'r');
if ($handle === false) {
throw new \RuntimeException("Não foi possível abrir: {$caminho}");
}
$linhas = [];
while (($linha = fgets($handle)) !== false) {
$linhas[] = trim($linha);
}
return $linhas;
} catch (\RuntimeException $e) {
echo "Erro: " . $e->getMessage() . "\n";
return [];
} finally {
// SEMPRE executa — com exceção, sem exceção, com return no try/catch
if (is_resource($handle)) {
fclose($handle);
echo "Arquivo fechado.\n";
}
}
}
// O finally executa mesmo quando há return no try
function demoFinally(): string
{
try {
return "valor do try"; // o return é preparado...
} finally {
echo "finally executou antes do return ser entregue!\n";
// Se houvesse return aqui, ele SOBRESCREVERIA o do try
}
}
echo demoFinally();
// finally executou antes do return ser entregue!
// valor do try
Handlers globais — a última linha de defesa
Qualquer exceção não capturada em nenhum try/catch sobe até o handler global registrado com set_exception_handler(). Esse handler formata a resposta de erro, registra no log, e decide o que mostrar ao usuário versus o que esconder. Em produção, nunca deve expor stack traces.
<?php
declare(strict_types=1);
use MeuApp\Exceptions\HttpException;
use MeuApp\Exceptions\ValidacaoException;
use MeuApp\Exceptions\AppException;
$ambiente = getenv('APP_ENV') ?: 'production';
// Registra o handler global para exceções não capturadas
set_exception_handler(function (\Throwable $e) use ($ambiente): void {
// 1. Sempre registra no log — independente do ambiente
error_log(sprintf(
"[%s] %s: %s em %s:%d\n%s",
date('Y-m-d H:i:s'),
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
));
// 2. Determina status HTTP e corpo baseados no tipo
[$status, $corpo] = match (true) {
$e instanceof ValidacaoException => [422, ['erros' => $e->erros]],
$e instanceof HttpException => [$e->statusHttp, ['mensagem' => $e->getMessage()]],
$e instanceof AppException => [500, ['mensagem' => $e->getMessage()]],
// Exceções inesperadas — não vaza detalhes em produção
default => [500, $ambiente === 'development'
? ['mensagem' => $e->getMessage(), 'classe' => get_class($e), 'trace' => $e->getTrace()]
: ['mensagem' => 'Erro interno do servidor. Tente novamente em instantes.']
],
};
// 3. Envia a resposta HTTP adequada
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['erro' => $corpo], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
});
// Converte erros PHP (notices, warnings) em exceções — tratamento uniforme
set_error_handler(function (int $errno, string $msg, string $file, int $line): bool {
// Respeita o operador @ (supressão de erros)
if (error_reporting() === 0) return false;
throw new \ErrorException($msg, $errno, $errno, $file, $line);
});
Boas práticas e antipadrões
Nunca capture para silenciar. Um catch vazio que engole a exceção sem log, sem retorno alternativo, sem nada — é o pior antipadrão possível. O erro desaparece, o sistema continua em estado inválido, e você não tem nenhuma pista do que aconteceu.
Não capture o que não sabe tratar. Se o código não sabe o que fazer com uma PDOException, não a capture ali — deixe subir para quem sabe. Capture somente no nível onde você tem contexto suficiente para tomar uma decisão significativa.
Use exceções para condições excepcionais, não para fluxo normal. buscarPorId() retornando null quando o registro não existe é fluxo normal — não é exceção. buscarPorId() lançando exceção quando o banco está inacessível é condição excepcional.
<?php
declare(strict_types=1);
// ❌ ANTIPADRÕES ───────────────────────────────────────────────────────
// 1. Catch vazio — engole o erro sem rastro
try {
$db->buscar(42);
} catch (\Exception $e) {
// silêncio total — o sistema continua em estado desconhecido
}
// 2. Re-lançar perdendo a causa original
try {
$pdo->query($sql);
} catch (\PDOException $e) {
// ❌ perde o stack trace original — $e some para sempre
throw new \RuntimeException("Erro ao buscar dados");
}
// 3. Usar exceções para fluxo normal
try {
$produto = $repo->buscarPorId($id); // lança se não encontrar
} catch (NaoEncontradoException $e) {
// ❌ "não encontrado" é fluxo normal — use retorno nullable
$produto = null;
}
// ✅ PADRÕES CORRETOS ──────────────────────────────────────────────────
// 1. Re-lançar preservando a causa
try {
$pdo->query($sql);
} catch (\PDOException $e) {
// ✅ $e fica disponível em getPrevious() para log e debug
throw new PedidoNaoEncontradoException($id, $e);
}
// 2. Finally para limpeza garantida
$conexao = null;
try {
$conexao = abrirConexao();
$conexao->executar($sql);
} finally {
// nullsafe para o caso de falhar no próprio abrirConexao()
$conexao?->fechar();
}
// 3. Exceção com dados ricos — mensagem descritiva e properties estruturadas
throw new EstoqueInsuficienteException(
produto: 'Teclado Mecânico',
solicitado: 5,
disponivel: 2,
);
// getMessage() → "Estoque insuficiente para 'Teclado Mecânico': solicitado 5, disponível 2."
// $e->produto, $e->solicitado, $e->disponivel acessíveis no handler
Resumo
| Conceito | O que aprendemos |
|---|---|
| Throwable | Interface raiz — Error (bugs PHP) vs Exception (erros de aplicação) |
catch (A|B $e) |
Múltiplos tipos num mesmo bloco — PHP 8+ |
| Hierarquia própria | Base abstrata por módulo + exceções específicas com readonly properties |
$previous |
Preserva a causa original ao traduzir exceções de infra para domínio |
getPrevious() |
Recupera a exceção que causou a atual — para log técnico |
finally |
Executa sempre — local correto para limpeza de recursos |
set_exception_handler() |
Handler global para exceções não capturadas |
set_error_handler() |
Converte warnings/notices em ErrorException — tratamento uniforme |
| Catch vazio | Antipadrão — nunca silenciar exceções sem log ou ação alternativa |
Exercício da semana
- Crie uma hierarquia de exceções para um sistema de pagamentos:
PagamentoException(base),CartaoRecusadoException(com código de recusa e últimos 4 dígitos),LimiteExcedidoException(com limite disponível e valor solicitado) eFraudeDetectadaException(com ID de transação). Cada exceção deve terreadonlyproperties e mensagem descritiva. - Implemente um
PagamentoServiceque pode lançar qualquer das exceções acima. Num ponto externo, trate cada tipo de forma diferente:CartaoRecusado→ solicitar novo cartão;LimiteExcedido→ sugerir parcelas;FraudeDetectada→ bloquear conta e notificar segurança. - Adicione encadeamento: quando um
PDOExceptionocorre ao registrar o pagamento, envolva-o numaPagamentoExceptionpreservando a causa. Verifique comgetPrevious()que o stack trace original está acessível. - Implemente um handler global com
set_exception_handler()que retorna JSON com status 402 paraPagamentoException, e 500 (sem detalhes em produção, com trace em desenvolvimento) para\Throwablegenérico. - Desafio: crie um
ExceptionHandlerorientado a objetos comregister(): void,render(\Throwable $e): arrayereport(\Throwable $e): void. Adicione suporte a renderers registráveis viaaddRenderer(string $classe, \Closure $renderer): voidpara que cada tipo de exceção tenha seu próprio formato de resposta.
Referências
- PHP Manual — Exceptions — Documentação oficial de exceções em PHP: sintaxe, herança, finally, exceções encadeadas.
- PHP Manual — Throwable — Referência completa da interface Throwable com todos os métodos: getMessage, getCode, getFile, getLine, getTrace, getPrevious.
- PHP Manual — SPL Exceptions — Hierarquia completa de exceções da SPL com descrição de quando usar cada subclasse de RuntimeException e LogicException.
- MARTIN, R.C. — Clean Code, Cap. 7: Error Handling. O'Reilly — Por que usar exceções em vez de códigos de retorno e o princípio de não retornar null.
- Laravel — Exception Handling — Como o Laravel estrutura seu handler de exceções: register(), renderable callbacks por tipo e integração com o kernel HTTP.
- PHP: The Right Way — Exceptions — Boas práticas consolidadas pela comunidade PHP sobre exceções.