3.3 Exceções e erros
Resumo
Exceções — tratamento de erro com try-catch-finally.
O essencial:
throw new Exception(), exceções customizadas, Error (PHP 7.0+), Throwable.Laravel:
abort()para HTTP,findOrFail()para model, Handler para tratamento global.
Conteúdo
- try-catch-finally
- throw (lançar exceção)
- Exceções customizadas
- Métodos de Exception
- Error (PHP 7.0+)
- set_exception_handler e set_error_handler
- Recapitulando
- Exercícios práticos
try-catch-finally
O que é: Mecanismo de tratamento de exceções.
Como funciona:
try {
// Código que pode lançar exceção
$user = User::findOrFail($id); // Lança ModelNotFoundException
$user->delete();
} catch (ModelNotFoundException $e) {
// Trata a exceção específica
echo "Usuário não encontrado: {$e->getMessage()}";
} catch (Exception $e) {
// Trata as demais exceções
echo "Erro: {$e->getMessage()}";
} finally {
// Roda sempre (mesmo se teve exceção)
DB::disconnect();
Log::info('Operação concluída');
}
// finally (PHP 5.5+)
try {
$file = fopen('file.txt', 'r');
// Trabalho com o arquivo
} finally {
if (isset($file)) {
fclose($file); // Fecha de qualquer jeito
}
}Quando usar: Erro que você consegue prever: arquivo não existe, banco fora.
Exemplo prático:
// Request de API com tratamento de erros
public function store(Request $request)
{
try {
DB::beginTransaction();
$user = User::create($request->validated());
$user->roles()->attach($request->input('roles'));
// API externa
$this->notificationService->send($user);
DB::commit();
return response()->json($user, 201);
} catch (ValidationException $e) {
DB::rollBack();
return response()->json(['errors' => $e->errors()], 422);
} catch (ApiException $e) {
DB::rollBack();
Log::error('API error', ['message' => $e->getMessage()]);
return response()->json(['error' => 'Falha ao enviar notificação'], 500);
} catch (\Exception $e) {
DB::rollBack();
Log::error('Unexpected error', ['message' => $e->getMessage()]);
return response()->json(['error' => 'Erro interno'], 500);
}
}
// Liberar recursos
public function processFile(string $path): array
{
$handle = fopen($path, 'r');
try {
$data = [];
while (($line = fgets($handle)) !== false) {
$data[] = json_decode($line, true);
}
return $data;
} finally {
fclose($handle); // Fecha de qualquer jeito
}
}Na entrevista:
"try-catch trata exceções. Dá para ter vários catch, um por tipo. finally roda sempre (para liberar recurso). No Laravel eu uso em transação, API externa e arquivo."
throw (lançar exceção)
O que é: Criar e lançar uma exceção.
Como funciona:
function divide(int $a, int $b): float
{
if ($b === 0) {
throw new InvalidArgumentException('Divisão por zero');
}
return $a / $b;
}
try {
$result = divide(10, 0);
} catch (InvalidArgumentException $e) {
echo $e->getMessage(); // "Divisão por zero"
}
// Exceções nativas do PHP
throw new Exception('Erro geral');
throw new RuntimeException('Erro de runtime');
throw new LogicException('Erro de lógica');
throw new InvalidArgumentException('Argumento inválido');
throw new OutOfBoundsException('Fora dos limites');
throw new BadMethodCallException('Chamada de método inválida');
// Com código de erro e previous exception
try {
$result = externalApi();
} catch (ApiException $e) {
throw new RuntimeException('Falha ao chamar a API', 500, $e);
}Quando usar: Quando o método não consegue seguir: validação falhou, recurso indisponível.
Exemplo prático:
// Validação no service
class OrderService
{
public function create(array $data): Order
{
if (empty($data['user_id'])) {
throw new InvalidArgumentException('User ID é obrigatório');
}
if ($data['amount'] <= 0) {
throw new InvalidArgumentException('Valor precisa ser positivo');
}
$user = User::find($data['user_id']);
if ($user === null) {
throw new RuntimeException("Usuário {$data['user_id']} não encontrado");
}
return Order::create($data);
}
}
// Eloquent findOrFail
$user = User::findOrFail($id); // Lança ModelNotFoundException
// abort() no Laravel (lança HttpException)
if (!auth()->check()) {
abort(401, 'Não autorizado');
}
if (!Gate::allows('update', $post)) {
abort(403, 'Acesso negado');
}
// Exceção customizada com contexto
class InsufficientFundsException extends Exception
{
public function __construct(
string $message,
private int $balance,
private int $required,
) {
parent::__construct($message);
}
public function getBalance(): int
{
return $this->balance;
}
public function getRequired(): int
{
return $this->required;
}
}
if ($wallet->balance < $amount) {
throw new InsufficientFundsException(
'Saldo insuficiente',
$wallet->balance,
$amount
);
}Na entrevista:
"throw lança a exceção. As nativas: Exception, RuntimeException, InvalidArgumentException. No Laravel: abort() para erro HTTP, findOrFail() lança ModelNotFoundException. Eu crio exceção customizada para regra de negócio."
Exceções customizadas
O que é: Classes de exceção suas, para erro específico.
Como funciona:
// Exceção customizada base
class OrderException extends Exception {}
class PaymentFailedException extends OrderException {}
class InsufficientStockException extends OrderException {}
// Uso
try {
$order = $this->createOrder($data);
$this->processPayment($order);
$this->reserveStock($order);
} catch (PaymentFailedException $e) {
// Trata erro de pagamento
$this->refundOrder($order);
throw $e;
} catch (InsufficientStockException $e) {
// Trata falta de estoque
$this->notifySupplier($e->getProduct());
} catch (OrderException $e) {
// Tratamento geral de erro do pedido
Log::error('Order error', ['message' => $e->getMessage()]);
}
// Com contexto extra
class ValidationException extends Exception
{
public function __construct(
string $message,
private array $errors,
) {
parent::__construct($message);
}
public function getErrors(): array
{
return $this->errors;
}
}
throw new ValidationException('Validação falhou', [
'email' => ['E-mail inválido'],
'password' => ['Senha muito curta'],
]);Quando usar: Erro de domínio, regra de negócio, caso específico.
Exemplo prático:
// Exceções HTTP no Laravel
namespace App\Exceptions;
use Exception;
class ApiException extends Exception
{
protected int $statusCode;
public function __construct(
string $message,
int $statusCode = 500,
?\Throwable $previous = null
) {
parent::__construct($message, 0, $previous);
$this->statusCode = $statusCode;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function render()
{
return response()->json([
'error' => $this->getMessage(),
], $this->statusCode);
}
}
// Uso
if (!$token) {
throw new ApiException('Token é obrigatório', 401);
}
// Exceções de domínio
class UserAlreadyExistsException extends Exception
{
public function __construct(string $email)
{
parent::__construct("Já existe usuário com o e-mail {$email}");
}
}
class OrderNotFoundException extends Exception
{
public function __construct(int $orderId)
{
parent::__construct("Pedido {$orderId} não encontrado");
}
}
// Service
public function register(array $data): User
{
$exists = User::where('email', $data['email'])->exists();
if ($exists) {
throw new UserAlreadyExistsException($data['email']);
}
return User::create($data);
}
// Handler
public function render($request, Throwable $exception)
{
if ($exception instanceof UserAlreadyExistsException) {
return response()->json([
'error' => $exception->getMessage(),
], 409);
}
if ($exception instanceof OrderNotFoundException) {
return response()->json([
'error' => $exception->getMessage(),
], 404);
}
return parent::render($request, $exception);
}Na entrevista:
"Exceção customizada é para erro de domínio. Eu herdo de Exception ou RuntimeException. Coloco contexto (balance, product). No Laravel crio ApiException, DomainException. O Handler trata e devolve JSON."
Métodos de Exception
O que é: Métodos do objeto Exception para pegar informação do erro.
Como funciona:
try {
throw new Exception('Mensagem de erro', 500);
} catch (Exception $e) {
// Métodos de Exception
echo $e->getMessage(); // "Mensagem de erro"
echo $e->getCode(); // 500
echo $e->getFile(); // /path/to/file.php
echo $e->getLine(); // 42
echo $e->getTrace(); // Array (stack trace)
echo $e->getTraceAsString(); // String (stack trace formatado)
echo $e->getPrevious(); // Previous exception (ou null)
// __toString()
echo $e; // Informação completa da exceção
}
// Previous exception (cadeia)
try {
throw new Exception('Erro original');
} catch (Exception $original) {
throw new RuntimeException('Erro encapsulado', 0, $original);
}
// Percorre a cadeia
try {
// ...
} catch (Exception $e) {
while ($e !== null) {
echo $e->getMessage() . "\n";
$e = $e->getPrevious();
}
}Quando usar: Log, debug, cadeia de exceções.
Exemplo prático:
// Log da exceção
try {
$this->externalApi->call();
} catch (ApiException $e) {
Log::error('API call failed', [
'message' => $e->getMessage(),
'code' => $e->getCode(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
]);
throw new RuntimeException('API externa falhou', 0, $e);
}
// Laravel Exception Handler
public function report(Throwable $exception)
{
if ($this->shouldReport($exception)) {
Log::error($exception->getMessage(), [
'exception' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
]);
// Envio para o Sentry
if (app()->bound('sentry')) {
app('sentry')->captureException($exception);
}
}
}
// Exceção customizada com contexto extra
class DatabaseException extends Exception
{
public function __construct(
string $message,
private string $query,
private array $bindings,
?\Throwable $previous = null
) {
parent::__construct($message, 0, $previous);
}
public function getQuery(): string
{
return $this->query;
}
public function getBindings(): array
{
return $this->bindings;
}
public function getContext(): array
{
return [
'message' => $this->getMessage(),
'query' => $this->query,
'bindings' => $this->bindings,
'file' => $this->getFile(),
'line' => $this->getLine(),
];
}
}
// Log com contexto
try {
DB::select($query, $bindings);
} catch (QueryException $e) {
$exception = new DatabaseException(
'Query no banco falhou',
$query,
$bindings,
$e
);
Log::error('Database error', $exception->getContext());
throw $exception;
}Na entrevista:
"Métodos de Exception: getMessage(), getCode(), getFile(), getLine(), getTrace(). getPrevious() para a cadeia. Uso no log e no Sentry. Na customizada eu coloco getContext() com informação extra."
Error (PHP 7.0+)
O que é: Erro fatal do PHP agora lança Error (dá para pegar).
Como funciona:
// PHP < 7.0: erro fatal (não dá para pegar)
// PHP 7.0+: lança Error (dá para pegar)
try {
nonExistentFunction(); // ParseError
} catch (Error $e) {
echo "Erro: {$e->getMessage()}";
}
try {
$obj->nonExistentMethod(); // Error
} catch (Error $e) {
echo "Erro: {$e->getMessage()}";
}
// Tipos de Error
try {
// Erros diferentes
} catch (ParseError $e) {
// Erro de sintaxe
} catch (TypeError $e) {
// Erro de tipo
} catch (ArithmeticError $e) {
// Erro aritmético
} catch (DivisionByZeroError $e) {
// Divisão por zero
} catch (Error $e) {
// Os demais Error
}
// Error vs Exception
// Error — erro interno do PHP
// Exception — exceção sua
// Throwable (PHP 7.0+)
try {
// ...
} catch (Throwable $e) {
// Pega Exception e Error
}Quando usar: Erro fatal que antes matava o script.
Exemplo prático:
// Tratamento de TypeError
function add(int $a, int $b): int
{
return $a + $b;
}
try {
$result = add(5, 'string'); // TypeError
} catch (TypeError $e) {
Log::error('Type error', ['message' => $e->getMessage()]);
return 0;
}
// Tratamento de DivisionByZeroError
try {
$result = intdiv(10, 0); // DivisionByZeroError
} catch (DivisionByZeroError $e) {
Log::error('Division by zero');
return null;
}
// Laravel Exception Handler (pega tudo)
public function render($request, Throwable $exception)
{
// Throwable pega Exception e Error
if ($exception instanceof ModelNotFoundException) {
return response()->json(['error' => 'Não encontrado'], 404);
}
if ($exception instanceof TypeError) {
Log::error('Type error', [
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]);
return response()->json(['error' => 'Erro interno'], 500);
}
return parent::render($request, $exception);
}
// Handler universal
set_exception_handler(function (Throwable $e) {
Log::error('Unhandled exception', [
'type' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
echo "Ocorreu um erro";
exit(1);
});Na entrevista:
"Error (PHP 7.0+) é o erro fatal do PHP, e agora dá para pegar. TypeError, ParseError, DivisionByZeroError. Throwable é a interface base de Exception e Error. No Handler do Laravel eu uso Throwable para tratar tudo."
set_exception_handler e set_error_handler
O que é: Handlers globais de exceção e erro.
Como funciona:
// Handler de exceção não capturada
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage());
echo "Ocorreu um erro. Tente de novo mais tarde.";
exit(1);
});
throw new Exception('Unhandled exception');
// Imprime: "Ocorreu um erro. Tente de novo mais tarde."
// Handler de erro do PHP (warnings, notices)
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
// Agora warning vira exceção
try {
$file = fopen('nonexistent.txt', 'r'); // Warning → Exception
} catch (ErrorException $e) {
echo "Erro de arquivo: {$e->getMessage()}";
}
// Restaura os handlers
restore_exception_handler();
restore_error_handler();Quando usar: Tratamento global: log, envio para o Sentry.
Exemplo prático:
// Laravel bootstrap/app.php (simplificado)
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
// app/Exceptions/Handler.php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
// Trata toda exceção não capturada
public function render($request, Throwable $exception)
{
// Log
$this->report($exception);
// JSON para API
if ($request->expectsJson()) {
return response()->json([
'error' => $exception->getMessage(),
], $this->getStatusCode($exception));
}
// HTML para o browser
return parent::render($request, $exception);
}
public function report(Throwable $exception)
{
// Envio para o Sentry
if (app()->bound('sentry') && $this->shouldReport($exception)) {
app('sentry')->captureException($exception);
}
parent::report($exception);
}
private function getStatusCode(Throwable $exception): int
{
return method_exists($exception, 'getStatusCode')
? $exception->getStatusCode()
: 500;
}
}
// Error handler customizado
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
// Ignora erro suprimido (@)
if (!(error_reporting() & $errno)) {
return false;
}
Log::warning('PHP error', [
'type' => $errno,
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
]);
// Converte em exceção
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});Na entrevista:
"set_exception_handler trata exceção não capturada. set_error_handler transforma warning/notice em exceção. O Laravel usa a classe Handler para o tratamento global. Eu mando para o Sentry, logo e devolvo JSON na API."
Recapitulando
O essencial:
try-catch-finally— tratamento de exceçõesthrow— lança a exceção- Nativas: Exception, RuntimeException, InvalidArgumentException
- Exceção customizada para erro de domínio
- Métodos: getMessage(), getCode(), getFile(), getLine(), getTrace()
Error(PHP 7.0+) — erro fatal (dá para pegar)Throwable— interface base (Exception + Error)set_exception_handler— handler global
Error vs Exception:
- Error — erro interno do PHP (TypeError, ParseError)
- Exception — exceção sua
Importante na entrevista:
- finally roda sempre (liberar recurso)
- Throwable pega Exception e Error
- Laravel: abort() para HTTP, findOrFail() para model
- Exceção customizada com contexto (getContext())
- Handler para tratamento global e envio ao Sentry
- Cadeia de exceções com getPrevious()
Exercícios práticos
Exercício 1: Crie uma exceção customizada com contexto
Enunciado: Crie a exceção InsufficientFundsException, que guarda o saldo, o valor pedido e expõe o método getContext().
Solução
<?php
namespace App\Exceptions;
use Exception;
class InsufficientFundsException extends Exception
{
public function __construct(
string $message,
private int $balance,
private int $required,
private string $currency = 'BRL',
) {
parent::__construct($message);
}
public function getBalance(): int
{
return $this->balance;
}
public function getRequired(): int
{
return $this->required;
}
public function getCurrency(): string
{
return $this->currency;
}
public function getShortage(): int
{
return $this->required - $this->balance;
}
public function getContext(): array
{
return [
'message' => $this->getMessage(),
'balance' => $this->balance,
'required' => $this->required,
'shortage' => $this->getShortage(),
'currency' => $this->currency,
'file' => $this->getFile(),
'line' => $this->getLine(),
];
}
public function render()
{
return response()->json([
'error' => 'insufficient_funds',
'message' => $this->getMessage(),
'balance' => $this->balance,
'required' => $this->required,
'shortage' => $this->getShortage(),
], 422);
}
}
// Uso
class WalletService
{
public function withdraw(Wallet $wallet, int $amount): void
{
if ($wallet->balance < $amount) {
throw new InsufficientFundsException(
'Saldo insuficiente na conta',
$wallet->balance,
$amount,
$wallet->currency
);
}
$wallet->balance -= $amount;
$wallet->save();
}
}
// Tratamento
try {
$walletService->withdraw($wallet, 10000);
} catch (InsufficientFundsException $e) {
Log::warning('Insufficient funds', $e->getContext());
return response()->json([
'error' => 'Saldo insuficiente',
'shortage' => $e->getShortage(),
], 422);
}Exercício 2: Implemente o tratamento global de erros
Enunciado: Crie um Laravel Exception Handler que loga todos os erros, manda os críticos para o Telegram e devolve JSON na API.
Solução
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class Handler extends ExceptionHandler
{
protected $dontReport = [
//
];
public function register(): void
{
$this->reportable(function (Throwable $e) {
// Log de todos os erros
Log::error($e->getMessage(), [
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
]);
// Erro crítico vai para o Telegram
if ($this->shouldReportToTelegram($e)) {
$this->sendToTelegram($e);
}
});
}
public function render($request, Throwable $e): JsonResponse|\Symfony\Component\HttpFoundation\Response
{
// JSON para request de API
if ($request->expectsJson()) {
return $this->renderJsonException($request, $e);
}
return parent::render($request, $e);
}
private function renderJsonException($request, Throwable $e): JsonResponse
{
$status = $this->getStatusCode($e);
$response = [
'error' => $this->getErrorMessage($e),
];
// No modo debug, inclui os detalhes
if (config('app.debug')) {
$response['exception'] = get_class($e);
$response['file'] = $e->getFile();
$response['line'] = $e->getLine();
$response['trace'] = explode("\n", $e->getTraceAsString());
}
return response()->json($response, $status);
}
private function getStatusCode(Throwable $e): int
{
if (method_exists($e, 'getStatusCode')) {
return $e->getStatusCode();
}
return match (true) {
$e instanceof NotFoundHttpException => 404,
$e instanceof \Illuminate\Auth\AuthenticationException => 401,
$e instanceof \Illuminate\Auth\Access\AuthorizationException => 403,
$e instanceof \Illuminate\Validation\ValidationException => 422,
default => 500,
};
}
private function getErrorMessage(Throwable $e): string
{
return match (true) {
$e instanceof NotFoundHttpException => 'Recurso não encontrado',
$e instanceof \Illuminate\Auth\AuthenticationException => 'Não autenticado',
$e instanceof \Illuminate\Auth\Access\AuthorizationException => 'Acesso negado',
default => config('app.debug') ? $e->getMessage() : 'Erro interno do servidor',
};
}
private function shouldReportToTelegram(Throwable $e): bool
{
// Só erro crítico
return $e instanceof \Error
|| $e instanceof \PDOException
|| $this->getStatusCode($e) >= 500;
}
private function sendToTelegram(Throwable $e): void
{
try {
$message = "🔴 *Erro em " . config('app.name') . "*\n\n";
$message .= "*Type:* " . get_class($e) . "\n";
$message .= "*Message:* " . $e->getMessage() . "\n";
$message .= "*File:* " . $e->getFile() . ":" . $e->getLine() . "\n";
$message .= "*URL:* " . request()->fullUrl();
// Envio para o Telegram (pacote ou HTTP client)
// TelegramService::send($message);
} catch (\Exception $telegramException) {
// Ignora erro de envio no Telegram
Log::warning('Failed to send to Telegram', [
'error' => $telegramException->getMessage(),
]);
}
}
}Exercício 3: Transações com exceções
Enunciado: Crie um service que cria o pedido e cobra. Se o pagamento falhar, faça rollback da transação e lance a exceção.
Solução
<?php
namespace App\Services;
use App\Exceptions\PaymentFailedException;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class OrderService
{
public function __construct(
private PaymentGateway $paymentGateway,
) {}
public function createAndPay(array $data): Order
{
return DB::transaction(function () use ($data) {
try {
// 1. Criar o pedido
$order = Order::create([
'user_id' => $data['user_id'],
'amount' => $data['amount'],
'status' => 'pending',
]);
Log::info('Order created', ['order_id' => $order->id]);
// 2. Reservar os produtos
$this->reserveProducts($order, $data['products']);
// 3. Cobrar o pagamento
$payment = $this->paymentGateway->charge(
$order->amount,
$data['payment_method']
);
// 4. Atualizar o status
$order->update([
'status' => 'paid',
'payment_id' => $payment->id,
]);
Log::info('Order paid', ['order_id' => $order->id]);
return $order;
} catch (PaymentFailedException $e) {
// Pagamento falhou — a transação dá rollback
Log::error('Payment failed', [
'order_id' => $order->id ?? null,
'error' => $e->getMessage(),
]);
throw $e; // A transação dá rollback sozinha
} catch (\Exception $e) {
// Qualquer outro erro
Log::error('Order creation failed', [
'error' => $e->getMessage(),
'data' => $data,
]);
throw new \RuntimeException(
'Falha ao criar o pedido: ' . $e->getMessage(),
0,
$e
);
}
});
}
private function reserveProducts(Order $order, array $products): void
{
foreach ($products as $productData) {
$product = Product::find($productData['id']);
if ($product->stock < $productData['quantity']) {
throw new \RuntimeException(
"Produto {$product->name} sem estoque"
);
}
$order->products()->attach($product->id, [
'quantity' => $productData['quantity'],
'price' => $product->price,
]);
$product->decrement('stock', $productData['quantity']);
}
}
}
// Uso
try {
$order = $orderService->createAndPay([
'user_id' => 1,
'amount' => 5000,
'products' => [
['id' => 1, 'quantity' => 2],
['id' => 2, 'quantity' => 1],
],
'payment_method' => 'card',
]);
return response()->json($order, 201);
} catch (PaymentFailedException $e) {
return response()->json([
'error' => 'payment_failed',
'message' => $e->getMessage(),
], 422);
} catch (\RuntimeException $e) {
return response()->json([
'error' => 'order_creation_failed',
'message' => $e->getMessage(),
], 500);
}Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate