PHP Exceções (try e catch)
Uma função descobre um problema que ela não sabe resolver: o cliente pediu 5 bolos e só há 2. Ela não deve mostrar mensagem nenhuma (talvez esteja num site, talvez num aplicativo), nem devolver um número mágico como -1 que alguém vai esquecer de conferir. O jeito moderno é lançar uma exceção: "aconteceu isto, quem me chamou que decida". Quem chamou captura a exceção e trata o problema do jeito certo para aquele lugar.
- Lançar uma exceção com
throw new Exception("…"). - O que acontece quando ninguém captura (o
Fatal error: Uncaught …). - Capturar com
try/catch, e o caminho que o programa percorre. finally: o código que roda sempre.Throwable×Exception×Error: quem captura o quê.- Vários
catch, e vários tipos numcatchsó com|. - O que tem dentro de uma exceção:
getMessage,getCode,getLine,getTraceAsString,getPrevious. throwcomo expressão (PHP 8.0+).
Antes, leia Tipos de Erro, onde você viu que muitos erros do PHP 8 já são exceções (TypeError, DivisionByZeroError…). Nesta aula, a lojinha da Ana lança e captura as suas.
A ideia: o alarme e quem atende
Pense na cozinha da lojinha. O ajudante vai buscar ovos e descobre que acabaram. Ele não sai da cozinha para ligar para o fornecedor, nem inventa uma receita sem ovos: ele avisa ("acabaram os ovos!") e quem coordena decide (comprar, trocar o pedido, avisar o cliente). Se ninguém ouvir o aviso, a cozinha para.
- Lançar (
throw, "atirar") = dar o aviso. - Capturar (
catch, "pegar") = ouvir o aviso e decidir. - Exceção = o aviso em si, um objeto com a mensagem e onde aconteceu.
Lançar: throw
<?php
function vender(int $estoque, int $quantidade): int
{
if ($quantidade > $estoque) {
throw new Exception("Estoque insuficiente: pediu $quantidade, tem $estoque");
}
return $estoque - $quantidade;
}
$resto = vender(10, 3);
echo "Restam $resto\n";
$resto = vender(2, 5);
echo "Restam $resto\n";
echo "Fim\n";
- throw
- "Lance". Interrompe a função na hora: nada depois do
throwé executado, nem oreturn. - new Exception(…)
- Cria o objeto exceção.
Exceptioné a classe de exceção mais geral do PHP (na próxima aula você cria as suas). - "Estoque insuficiente: …"
- A mensagem: escreva para quem vai ler o registro de erros, com os valores que ajudam a entender.
$ cd exc
$ php lancar.php; echo "código: $?"
Restam 7
Fatal error: Uncaught Exception: Estoque insuficiente: pediu 5, tem 2 in /home/ana/exc/lancar.php:5
Stack trace:
#0 /home/ana/exc/lancar.php(12): vender(2, 5)
#1 {main}
thrown in /home/ana/exc/lancar.php on line 5
código: 255
A primeira venda funcionou. Na segunda, a exceção foi lançada e ninguém capturou. Uma exceção não capturada "sobe" por todas as funções até o topo do programa e, lá, o PHP a transforma num erro fatal. Lendo a mensagem:
| Pedaço | Significa |
|---|---|
Uncaught Exception | "Exceção não capturada", do tipo Exception. |
Estoque insuficiente: pediu 5, tem 2 | A mensagem que você escreveu. |
in /home/ana/exc/lancar.php:5 | Onde foi criada (a linha do throw new). |
Stack trace: | "Rastro da pilha": o caminho de chamadas até ali, do mais recente para o mais antigo. |
#0 /home/ana/exc/lancar.php(12): vender(2, 5) | A linha 12 chamou vender com os argumentos 2, 5. |
#1 {main} | O programa principal (fora de funções). |
Capturar: try e catch
<?php
// … a mesma função vender …
try {
$resto = vender(10, 3);
echo "Restam $resto\n";
$resto = vender(2, 5);
echo "Esta linha não roda\n";
} catch (Exception $e) {
echo "Não deu: ", $e->getMessage(), "\n";
}
echo "Fim\n";
- try { … }
- "Tente": o código que pode lançar uma exceção.
- catch (Exception $e)
- "Capture exceções do tipo
Exceptione guarde-a em$e". O nome$eé costume; pode ser qualquer um. - { … }
- O que fazer com o problema. Aqui, mostrar a mensagem.
- $e->getMessage()
- Devolve o texto passado ao
new Exception(…).
$ php try.php Restam 7 Não deu: Estoque insuficiente: pediu 5, tem 2 Fim « o programa continuou depois do catch
Siga o caminho do programa:
- Entra no
try. A primeira venda dá certo e mostraRestam 7. - A segunda venda lança a exceção. O PHP abandona o resto do
try(a linha "Esta linha não roda" é pulada) e procura umcatchque sirva. - O
catch (Exception $e)serve: o bloco dele roda. - Depois do
catch, o programa segue normalmente:Fim.
Se nenhuma exceção acontecer, o catch é simplesmente ignorado.
Missão: o programa morre na segunda venda. Envolva as vendas num try/catch que mostre Pedido recusado: seguido da mensagem da exceção, para que o Fim apareça.
<?php
function vender(int $estoque, int $quantidade): int
{
if ($quantidade > $estoque) {
throw new Exception("Estoque insuficiente: pediu $quantidade, tem $estoque");
}
return $estoque - $quantidade;
}
$resto = vender(10, 3);
echo "Restam $resto\n";
$resto = vender(2, 5);
echo "Restam $resto\n";
echo "Fim\n";
<?php
function vender(int $estoque, int $quantidade): int
{
if ($quantidade > $estoque) {
throw new Exception("Estoque insuficiente: pediu $quantidade, tem $estoque");
}
return $estoque - $quantidade;
}
try {
$resto = vender(10, 3);
echo "Restam $resto\n";
$resto = vender(2, 5);
echo "Restam $resto\n";
} catch (Exception $e) {
echo "Pedido recusado: " . $e->getMessage() . "\n";
}
echo "Fim\n";
finally: o que roda sempre
Às vezes há uma "arrumação" que precisa acontecer com ou sem erro: fechar um arquivo, liberar uma trava, fechar o caixa. O bloco finally ("finalmente") roda sempre, depois do try e do catch:
<?php
function processar(int $qtd): void
{
echo "Abrindo o caixa\n";
try {
if ($qtd <= 0) {
throw new Exception("Quantidade inválida: $qtd");
}
echo "Vendidos: $qtd\n";
} catch (Exception $e) {
echo "Erro: ", $e->getMessage(), "\n";
} finally {
echo "Fechando o caixa\n";
}
}
processar(3);
echo "---\n";
processar(0);
$ php finally.php Abrindo o caixa Vendidos: 3 Fechando o caixa « sem erro: try → finally --- Abrindo o caixa Erro: Quantidade inválida: 0 Fechando o caixa « com erro: try → catch → finally
O finally roda até quando o try faz return, e até quando a exceção não é capturada ali (ela continua subindo depois do finally):
<?php
function teste(): string
{
try {
return "do try";
} finally {
echo "o finally roda mesmo com return\n";
}
}
echo teste(), "\n";
$ php finally-return.php o finally roda mesmo com return do try
Repare que esse try não tem catch: try + finally sozinhos também valem.
Missão: hoje o caixa só é fechado quando a venda dá certo. Mova o echo "Fechando o caixa\n"; para um bloco finally, para ele aparecer nas três chamadas.
<?php
function processar(int $qtd): void
{
echo "Abrindo o caixa\n";
try {
if ($qtd <= 0) {
throw new Exception("Quantidade inválida: $qtd");
}
echo "Vendidos: $qtd\n";
echo "Fechando o caixa\n";
} catch (Exception $e) {
echo "Erro: ", $e->getMessage(), "\n";
}
}
processar(3);
processar(0);
processar(-1);
<?php
function processar(int $qtd): void
{
echo "Abrindo o caixa\n";
try {
if ($qtd <= 0) {
throw new Exception("Quantidade inválida: $qtd");
}
echo "Vendidos: $qtd\n";
} catch (Exception $e) {
echo "Erro: ", $e->getMessage(), "\n";
} finally {
echo "Fechando o caixa\n";
}
}
processar(3);
processar(0);
processar(-1);
Exception, Error e Throwable
Lembra da aula anterior? No PHP 8 há duas famílias de coisas que podem ser lançadas:
| Família | Quem lança | Exemplos |
|---|---|---|
Exception | O seu código e as bibliotecas, para problemas "esperados" (dado inválido, arquivo ruim, regra de negócio) | Exception, InvalidArgumentException, RuntimeException, JsonException |
Error | O próprio PHP, para erros de programação | TypeError, ValueError, DivisionByZeroError, função inexistente |
As duas são "filhas" da interface Throwable ("lançável"). E um catch só pega o tipo que ele nomeia e os filhos dele. Por isso, catch (Exception $e) não pega um Error:
<?php
try {
echo intdiv(10, 0);
} catch (Exception $e) {
echo "Peguei: ", $e->getMessage(), "\n";
}
echo "Fim\n";
$ php exception-error.php
Fatal error: Uncaught DivisionByZeroError: Division by zero in /home/ana/exc/exception-error.php:3
Stack trace:
#0 /home/ana/exc/exception-error.php(3): intdiv(10, 0)
#1 {main}
thrown in /home/ana/exc/exception-error.php on line 3
O catch foi ignorado, porque DivisionByZeroError é da família Error. Para pegar qualquer coisa, capture Throwable:
<?php
try {
echo intdiv(10, 0);
} catch (Throwable $e) {
echo "Peguei ", get_class($e), ": ", $e->getMessage(), "\n";
}
var_dump($e instanceof Error, $e instanceof Exception, $e instanceof Throwable);
$ php throwable.php Peguei DivisionByZeroError: Division by zero bool(true) « é um Error bool(false) « não é uma Exception bool(true) « e todo mundo é Throwable
get_class($e) diz a classe exata da exceção; instanceof ("é instância de") pergunta se ela pertence a uma família.
Capture o tipo mais específico que você sabe tratar (JsonException, InvalidArgumentException). Capturar Throwable esconde erros de programação, que deveriam aparecer e ser consertados. Ele é útil num ponto só, "lá no alto" do programa, para mostrar uma página de erro amigável e registrar o problema.
Vários tipos de exceção
Vários catch
Um try pode ter vários catch, um para cada tipo, cada um com seu tratamento. O PHP testa de cima para baixo e usa o primeiro que servir:
<?php
function dividir_conta(float $total, int $pessoas): float
{
if ($total < 0) {
throw new InvalidArgumentException('Total negativo');
}
return $total / $pessoas;
}
foreach ([[90, 3], [-5, 2], [90, 0]] as [$t, $p]) {
try {
$parte = dividir_conta($t, $p);
echo "Cada um paga $parte\n";
} catch (InvalidArgumentException $e) {
echo "Valor errado: ", $e->getMessage(), "\n";
} catch (DivisionByZeroError $e) {
echo "Ninguém para pagar: ", $e->getMessage(), "\n";
} catch (Throwable $e) {
echo "Outro problema: ", $e->getMessage(), "\n";
}
}
$ php varios.php Cada um paga 30 Valor errado: Total negativo Ninguém para pagar: Division by zero
Como o primeiro que serve ganha, ponha os tipos específicos primeiro e os gerais depois. Ao contrário, o geral "rouba" tudo:
<?php
try {
throw new InvalidArgumentException('x');
} catch (Exception $e) {
echo "Caiu no catch de Exception\n";
} catch (InvalidArgumentException $e) {
echo "Caiu no catch específico\n";
}
$ php ordem.php Caiu no catch de Exception « InvalidArgumentException é filha de Exception: o primeiro serviu
Vários tipos num catch só: |
Quando o tratamento é o mesmo para tipos diferentes, junte-os com | ("ou"):
<?php
function ler_pedido(string $json): array
{
$dados = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
if (!isset($dados['quantidade'])) {
throw new InvalidArgumentException('Falta a quantidade');
}
return $dados;
}
foreach (['{"quantidade": 2}', '{"quantidade": ', '{"produto": "bolo"}', '[1, 2]'] as $json) {
try {
$p = ler_pedido($json);
echo "OK: {$p['quantidade']} unidade(s)\n";
} catch (JsonException | InvalidArgumentException $e) {
echo "Pedido recusado (", get_class($e), "): ", $e->getMessage(), "\n";
}
}
$ php multi.php OK: 2 unidade(s) Pedido recusado (JsonException): Syntax error « o JSON quebrado (aula JSON) Pedido recusado (InvalidArgumentException): Falta a quantidade Pedido recusado (InvalidArgumentException): Falta a quantidade
As exceções prontas do PHP
Antes de criar as suas (próxima aula), saiba que o PHP já tem várias, com nomes que explicam o problema. As mais usadas:
| Classe | Use quando… |
|---|---|
InvalidArgumentException | Um argumento recebido é inválido (quantidade negativa, e-mail vazio). |
DomainException | O valor não faz sentido no assunto do programa (um desconto de 150%). |
LengthException | Tamanho errado (CPF com 9 dígitos). |
RangeException / OutOfRangeException | Valor fora da faixa permitida (nota 11) / índice que não existe. |
RuntimeException | Algo deu errado durante a execução e não é culpa do código (disco cheio, serviço fora do ar). |
LogicException | Erro de lógica do programa, que deveria ser consertado no código. |
UnexpectedValueException | Uma função devolveu um valor inesperado. |
O que tem dentro de uma exceção
A exceção é um objeto com bastante informação. Os métodos (todos começam com get, "pegar"):
<?php
function validar(int $nota): void
{
if ($nota < 0 || $nota > 10) {
throw new RangeException("Nota fora da faixa: $nota", 42);
}
}
function lancar_notas(array $notas): void
{
foreach ($notas as $n) {
validar($n);
}
}
try {
lancar_notas([7, 9, 11]);
} catch (RangeException $e) {
echo "Mensagem: ", $e->getMessage(), "\n";
echo "Código: ", $e->getCode(), "\n";
echo "Arquivo: ", $e->getFile(), "\n";
echo "Linha: ", $e->getLine(), "\n";
echo "Rastro:\n", $e->getTraceAsString(), "\n";
}
$ php metodos.php
Mensagem: Nota fora da faixa: 11
Código: 42 « o 2º argumento do new (padrão: 0)
Arquivo: /home/ana/exc/metodos.php
Linha: 5 « onde a exceção foi criada
Rastro:
#0 /home/ana/exc/metodos.php(11): validar(11)
#1 /home/ana/exc/metodos.php(15): lancar_notas(Array)
#2 {main}
| Método | Devolve |
|---|---|
getMessage() | A mensagem. |
getCode() | Um código numérico opcional (o 2º argumento do construtor). |
getFile() / getLine() | Arquivo e linha onde a exceção foi criada. |
getTraceAsString() | O rastro de chamadas, como texto (ótimo para o registro de erros). |
getTrace() | O mesmo rastro, como array. |
getPrevious() | A exceção "anterior", que causou esta (ou null). |
E se você der echo na exceção inteira, o PHP mostra tudo junto, no mesmo formato do erro fatal:
$ php tostring.php « catch (LogicException $e) { echo $e, "\n"; }
LogicException: Desconto maior que o preço in /home/ana/exc/tostring.php:3
Stack trace:
#0 {main}
Uma exceção causada por outra: getPrevious
Às vezes você captura uma exceção "técnica" e lança outra com uma mensagem mais útil, guardando a original como causa (o 3º argumento do construtor):
<?php
function carregar_config(string $arquivo): array
{
try {
return json_decode(file_get_contents($arquivo), true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new RuntimeException("Configuração inválida em $arquivo", 0, $e);
}
}
file_put_contents('config.json', '{"frete": 12,5}');
try {
carregar_config('config.json');
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n";
echo "Causa: ", $e->getPrevious()->getMessage(), "\n";
}
$ php previous.php Configuração inválida em config.json Causa: Syntax error « 12,5 não é número em JSON
Capturar, anotar e relançar
Um catch pode fazer alguma coisa (como registrar) e lançar a mesma exceção de novo com throw $e;, para quem estiver acima decidir:
<?php
function salvar(): void
{
try {
throw new RuntimeException('Disco cheio');
} catch (RuntimeException $e) {
error_log('Falha ao salvar: ' . $e->getMessage());
throw $e;
}
}
salvar();
$ php rethrow.php
Falha ao salvar: Disco cheio « o error_log (no terminal, sem arquivo de log, sai na tela)
Fatal error: Uncaught RuntimeException: Disco cheio in /home/ana/exc/rethrow.php:5
Stack trace:
#0 /home/ana/exc/rethrow.php(11): salvar()
#1 {main}
thrown in /home/ana/exc/rethrow.php on line 5
Novidades do PHP 8
throw como expressão PHP 8.0+
Antes, throw era uma instrução e só podia ficar sozinho numa linha. Desde o PHP 8.0 ele é uma expressão, e cabe em lugares como depois do ??, num ternário ou numa arrow function:
<?php
function preco(array $produto): float
{
return $produto['preco'] ?? throw new InvalidArgumentException("Produto {$produto['nome']} sem preço");
}
echo preco(['nome' => 'Bolo', 'preco' => 35.0]), "\n";
try {
echo preco(['nome' => 'Torta']), "\n";
} catch (InvalidArgumentException $e) {
echo $e->getMessage(), "\n";
}
$desconto = fn(float $p) => $p > 0 ? $p * 0.9 : throw new ValueError('Preço deve ser positivo');
echo $desconto(10), "\n";
$ php expressao.php 35 Produto Torta sem preço 9
Lê-se: "devolva o preço; se não houver (??), lance a exceção". Em versões antigas, seria um if (!isset(…)) { throw …; } antes do return.
catch sem variável PHP 8.0+
Se você não vai usar o $e, pode omiti-lo:
<?php
try {
$dados = json_decode('{x}', flags: JSON_THROW_ON_ERROR);
} catch (JsonException) {
echo "JSON inválido, usando valores padrão\n";
$dados = [];
}
$ php semvar.php JSON inválido, usando valores padrão
Exceção × devolver false
Muitas funções antigas do PHP avisam o erro devolvendo false (como file_get_contents). Por que as funções novas, e as suas, preferem exceções?
Devolver false/null | Lançar exceção | |
|---|---|---|
| Se quem chamou esquecer de conferir | O programa segue com um valor errado e quebra longe dali | O programa para com uma mensagem clara, na linha certa |
| Explicar o motivo | Não dá (é só false) | Mensagem, código, tipo, causa |
| Tratar em outro lugar | Cada nível de função precisa repassar o false | A exceção sobe sozinha até quem tiver um catch |
| Quando usar | Quando "não achei" é um resultado normal (procurar um item numa lista) | Quando é um problema de verdade |
Quando dá errado
Fatal error: Uncaught Exception: Estoque insuficiente: pediu 5, tem 2 in /home/ana/exc/lancar.php:5
O que significa: uma exceção foi lançada e nenhum catch no caminho a capturou.
Como consertar: veja no Stack trace qual chamada levou ao problema e coloque um try/catch no nível que sabe o que fazer (mostrar uma mensagem ao cliente, por exemplo). Ou conserte a causa, se for erro de programação.
catch não pegou a exceção
O que significa: o tipo no catch não é o da exceção nem um "pai" dela. O caso clássico: catch (Exception $e) com um TypeError ou DivisionByZeroError, que são da família Error.
Como consertar: capture o tipo certo (a mensagem de erro diz qual: Uncaught DivisionByZeroError), ou Throwable se for mesmo para pegar tudo.
$ php throwstring.php « throw "Estoque insuficiente";
Fatal error: Uncaught Error: Can only throw objects in /home/ana/exc/throwstring.php:2
Stack trace:
#0 {main}
thrown in /home/ana/exc/throwstring.php on line 2
O que significa: "só é possível lançar objetos". Um texto não é uma exceção.
Como consertar: throw new Exception("Estoque insuficiente");.
catch vazio "engolindo" erros
try { … } catch (Throwable $e) { } faz qualquer problema sumir sem deixar rastro. O programa continua com dados pela metade e ninguém sabe por quê.
Como consertar: todo catch deve fazer algo: tratar, registrar (error_log($e)) ou relançar.
Referência
| Sintaxe | O que faz |
|---|---|
throw new Exception($msg, $codigo, $anterior); | Lança uma exceção. |
try { … } catch (Tipo $e) { … } | Captura exceções do tipo (e filhos). |
catch (TipoA | TipoB $e) | Captura qualquer um dos tipos. |
catch (Tipo) 8.0+ | Captura sem guardar em variável. |
finally { … } | Roda sempre, com ou sem exceção. |
throw $e; | Relança a exceção capturada. |
$x ?? throw new … 8.0+ | throw como expressão. |
Throwable → Exception / Error | As duas famílias. |
getMessage, getCode, getFile, getLine, getTraceAsString, getPrevious | Informações da exceção. |
Exercícios
1. Lançar
Complete para lançar uma exceção com a mensagem "Sem estoque":
2. Pegar tudo
Complete para capturar exceções e erros do PHP:
3. A mensagem
Complete para mostrar a mensagem da exceção:
4. Num try, a linha 2 lança uma exceção que é capturada. O que acontece com a linha 3 do try?
5. catch (Exception $e) captura um TypeError?
6. Quando o bloco finally roda?
Missão final: complete a função ler_linha. Ela recebe um texto como "bolo;3" e devolve ['bolo', 3]. Se faltar a quantidade, lance InvalidArgumentException com Falta a quantidade; se a quantidade não for um número inteiro positivo, lance InvalidArgumentException com Quantidade inválida: …. No laço, capture e mostre Linha N recusada: mensagem, e no fim mostre o total das linhas válidas (Total: 8 unidade(s)). Dica: ctype_digit('3') é true; ctype_digit('-2') e ctype_digit('abc') são false.
<?php
function ler_linha(string $linha): array
{
$partes = explode(';', $linha);
// seu código aqui
}
$pedido = ['bolo;3', 'torta;abc', 'brigadeiro;-2', 'pudim', 'cocada;5'];
$total = 0;
foreach ($pedido as $i => $linha) {
[$produto, $qtd] = ler_linha($linha);
echo "$produto: $qtd unidade(s)\n";
$total += $qtd;
}
echo "Total: $total unidade(s)\n";
<?php
function ler_linha(string $linha): array
{
$partes = explode(';', $linha);
$qtd = $partes[1] ?? throw new InvalidArgumentException('Falta a quantidade');
if (!ctype_digit($qtd) || (int) $qtd === 0) {
throw new InvalidArgumentException("Quantidade inválida: $qtd");
}
return [$partes[0], (int) $qtd];
}
$pedido = ['bolo;3', 'torta;abc', 'brigadeiro;-2', 'pudim', 'cocada;5'];
$total = 0;
foreach ($pedido as $i => $linha) {
try {
[$produto, $qtd] = ler_linha($linha);
echo "$produto: $qtd unidade(s)\n";
$total += $qtd;
} catch (InvalidArgumentException $e) {
echo "Linha " . ($i + 1) . " recusada: " . $e->getMessage() . "\n";
}
}
echo "Total: $total unidade(s)\n";
Resumo
throw new Exception("mensagem")interrompe a função e "sobe" até umcatch; sem nenhum, viraFatal error: Uncaught ….try { … } catch (Tipo $e) { … }: se algo notrylançar, o resto dotryé pulado e ocatchque servir roda.finallyroda sempre.Throwableé a raiz;Exceptioné do seu código,Erroré do PHP.catch (Exception)não pegaError.- Vários
catch(do específico ao geral) oucatch (A | B $e). getMessage,getCode,getLine,getTraceAsString,getPreviouscontam o que houve.- No PHP 8,
throwé expressão (?? throw …) e ocatchpode vir sem variável.
Próxima aula: Criar suas Exceções, como a EstoqueInsuficienteException da lojinha.