Error Handling
Error handling decides what users see and what the team learns. Laravel reports exceptions to logs and renders a response based on the request type: an error page for browsers, JSON for API clients.
Throwing errors
abort(404);
abort(403, 'You cannot view this invoice.');
abort_if(! $user->active, 403);
abort_unless($user->can('view', $invoice), 403);
For domain failures, throw a dedicated exception. It documents intent and can be rendered consistently.
namespace App\Exceptions;
use RuntimeException;
class PaymentDeclined extends RuntimeException
{
public function __construct(public readonly string $reason)
{
parent::__construct('Payment declined: '.$reason);
}
}
Rendering by exception type
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (PaymentDeclined $e, Request $request) {
return $request->expectsJson()
? response()->json(['message' => $e->reason], 402)
: back()->withErrors(['payment' => $e->reason]);
});
})
Custom error views
Place 403.blade.php, 404.blade.php, or 500.blade.php in resources/views/errors. Laravel uses them for HTTP exceptions and, when debug mode is off, for uncaught errors. Keep these pages simple and free of sensitive details.
Logging versus displaying
Users need a short message and a way forward. Operators need a request identifier, stack trace, and context. Do not put exception messages or SQL fragments into a production error page. Log the details instead and show a generic explanation.
Recoverable failures
Not every failure is an exception. A failed third-party sync can be reported and retried. A malformed CSV row can be collected and shown in an import report. Reserve exceptions for conditions that break the current operation.
try {
$crm->push($customer);
} catch (ConnectionException $e) {
report($e);
$customer->update(['sync_pending' => true]);
}
Testing failure paths
$this->get('/invoices/999')
->assertNotFound();
$this->actingAs($user)
->get('/admin')
->assertForbidden();
$this->post('/payments', $payload)
->assertStatus(402)
->assertJson(['message' => 'Card expired']);
Production checklist
APP_DEBUG=falseand a custom 500 page.- Log level set so warnings survive but debug noise does not fill the disk.
- Error reporting to a service that groups duplicate exceptions.
- A request identifier echoed to the user and stored with each log entry.
- Alerts on new exception types, not only on volume spikes.