Introduction to Error Handling
Proper error handling and logging are crucial for building robust Laravel applications. Laravel provides a comprehensive exception handling system built on top of Symfony's error handler, making it easy to manage errors, log exceptions, and provide user-friendly error pages.
Note: Laravel distinguishes between exceptions (errors that can be caught and handled) and fatal errors (which terminate the application). The exception handler is your central point for managing application errors.
The Exception Handler
All exceptions are handled by the App\Exceptions\Handler class:
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types that are not reported.
*/
protected $dontReport = [
// Exceptions that should not be logged
];
/**
* A list of inputs that should never be flashed to the session on validation errors.
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register exception handling callbacks.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
// Custom reporting logic
});
}
}
Throwing Exceptions
Laravel provides multiple ways to throw exceptions:
<?php
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\HttpException;
// Throw basic exception
throw new \Exception('Something went wrong');
// Throw with HTTP status code using abort()
abort(404); // Not Found
abort(403, 'Unauthorized action');
abort(500, 'Server error occurred');
// abort_if() - Conditional abort
abort_if(! Auth::check(), 403, 'You must be logged in');
abort_if($user->banned, 403, 'Your account has been banned');
// abort_unless() - Opposite of abort_if
abort_unless(Auth::user()->is_admin, 403);
// Throw HTTP exception manually
throw new HttpException(403, 'Access denied');
// Validation exception (automatically thrown by validation)
// Returns 422 Unprocessable Entity with error messages
HTTP Exceptions
Laravel provides shortcuts for common HTTP error responses:
<?php
// 400 Bad Request
abort(400, 'Invalid request format');
// 401 Unauthorized
abort(401, 'Authentication required');
// 403 Forbidden
abort(403, 'You do not have permission');
// 404 Not Found
abort(404, 'Resource not found');
// 419 Page Expired (CSRF token mismatch)
abort(419);
// 422 Unprocessable Entity (validation errors)
abort(422, 'Validation failed');
// 429 Too Many Requests (rate limiting)
abort(429, 'Rate limit exceeded');
// 500 Internal Server Error
abort(500, 'Server error occurred');
// 503 Service Unavailable (maintenance mode)
abort(503, 'Application under maintenance');
Tip: Use abort() for expected errors (user not found, permission denied) and throw regular exceptions for unexpected errors (database connection failed, external API error).
Custom Exceptions
Create custom exception classes for specific error scenarios:
# Create custom exception
php artisan make:exception PaymentFailedException
<?php
namespace App\Exceptions;
use Exception;
class PaymentFailedException extends Exception
{
/**
* The payment transaction ID.
*/
protected $transactionId;
/**
* Create a new exception instance.
*/
public function __construct($message = 'Payment processing failed', $transactionId = null)
{
parent::__construct($message);
$this->transactionId = $transactionId;
}
/**
* Get the transaction ID.
*/
public function getTransactionId()
{
return $this->transactionId;
}
/**
* Report the exception (logging).
*/
public function report()
{
Log::error('Payment failed', [
'transaction_id' => $this->transactionId,
'message' => $this->getMessage(),
]);
}
/**
* Render the exception as an HTTP response.
*/
public function render($request)
{
if ($request->expectsJson()) {
return response()->json([
'error' => 'Payment failed',
'message' => $this->getMessage(),
'transaction_id' => $this->transactionId,
], 422);
}
return redirect()->back()
->with('error', $this->getMessage())
->withInput();
}
}
// Usage
throw new PaymentFailedException('Credit card declined', $transactionId);
Catching and Handling Exceptions
Use try-catch blocks to handle exceptions gracefully:
<?php
use App\Exceptions\PaymentFailedException;
use Illuminate\Database\QueryException;
use Exception;
public function processPayment($orderId)
{
try {
// Attempt payment processing
$payment = PaymentGateway::charge($orderId);
return response()->json([
'success' => true,
'payment_id' => $payment->id,
]);
} catch (PaymentFailedException $e) {
// Handle payment-specific errors
Log::error('Payment failed: ' . $e->getMessage());
return response()->json([
'error' => 'Payment failed',
'message' => $e->getMessage(),
], 422);
} catch (QueryException $e) {
// Handle database errors
Log::error('Database error: ' . $e->getMessage());
return response()->json([
'error' => 'Database error occurred',
], 500);
} catch (Exception $e) {
// Handle all other exceptions
Log::error('Unexpected error: ' . $e->getMessage());
return response()->json([
'error' => 'An unexpected error occurred',
], 500);
} finally {
// Always execute (cleanup code)
Cache::forget('processing_payment_' . $orderId);
}
}
Custom Error Pages
Create custom views for HTTP error codes in resources/views/errors/:
<!-- resources/views/errors/404.blade.php -->
@extends('layouts.app')
@section('content')
<div class="error-page">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="{{ route('home') }}">Go to Homepage</a>
</div>
@endsection
<!-- resources/views/errors/403.blade.php -->
@extends('layouts.app')
@section('content')
<div class="error-page">
<h1>403 - Access Denied</h1>
<p>{{ $exception->getMessage() ?: 'You don\'t have permission to access this resource.' }}</p>
<a href="{{ route('home') }}">Go Back</a>
</div>
@endsection
Logging in Laravel
Laravel uses the powerful Monolog library for logging. Configuration is in config/logging.php:
<?php
return [
'default' => env('LOG_CHANNEL', 'stack'),
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'slack'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => env('LOG_LEVEL', 'critical'),
],
'stderr' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'with' => [
'stream' => 'php://stderr',
],
],
],
];
Writing Log Messages
Use the Log facade to write messages at different severity levels:
<?php
use Illuminate\Support\Facades\Log;
// Emergency: system is unusable
Log::emergency('System is down');
// Alert: action must be taken immediately
Log::alert('Database server is unreachable');
// Critical: critical conditions
Log::critical('Application component unavailable');
// Error: error conditions
Log::error('Payment processing failed', [
'user_id' => $userId,
'amount' => $amount,
]);
// Warning: warning conditions
Log::warning('API rate limit approaching');
// Notice: normal but significant condition
Log::notice('User logged in from new IP address');
// Info: informational messages
Log::info('Order placed successfully', [
'order_id' => $order->id,
]);
// Debug: debug-level messages
Log::debug('Cache miss for key: ' . $cacheKey);
// Log to specific channel
Log::channel('slack')->critical('Payment gateway is down');
// Log to multiple channels
Log::stack(['single', 'slack'])->error('Critical error occurred');
Note: Log levels follow RFC 5424 specification. Use appropriate levels: emergency/alert/critical for urgent issues, error for errors, warning for warnings, info for general information, debug for development.
Contextual Logging
Add context to log messages for better debugging:
<?php
// Log with context array
Log::info('User action', [
'user_id' => auth()->id(),
'action' => 'purchase',
'product_id' => $product->id,
'ip_address' => request()->ip(),
'timestamp' => now(),
]);
// Log exception with stack trace
try {
// Some code that may fail
} catch (Exception $e) {
Log::error('Exception occurred', [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
]);
}
// Log with request context
Log::info('API request', [
'method' => request()->method(),
'url' => request()->fullUrl(),
'params' => request()->all(),
'headers' => request()->headers->all(),
]);
Conditional Logging
Log messages only in specific environments or conditions:
<?php
// Log only in production
if (app()->environment('production')) {
Log::error('Production error occurred');
}
// Log only when debugging
if (config('app.debug')) {
Log::debug('Debug information', ['data' => $data]);
}
// Log slow queries
if ($executionTime > 1000) {
Log::warning('Slow query detected', [
'query' => $query,
'time' => $executionTime . 'ms',
]);
}
// Log failed login attempts
if ($loginFailed) {
Log::notice('Failed login attempt', [
'email' => $request->email,
'ip' => $request->ip(),
]);
}
Custom Log Channels
Create custom log channels for specific purposes:
<?php
// config/logging.php
'channels' => [
'payments' => [
'driver' => 'daily',
'path' => storage_path('logs/payments.log'),
'level' => 'info',
'days' => 30,
],
'security' => [
'driver' => 'daily',
'path' => storage_path('logs/security.log'),
'level' => 'warning',
'days' => 90,
],
'api' => [
'driver' => 'daily',
'path' => storage_path('logs/api.log'),
'level' => 'debug',
'days' => 7,
],
];
// Usage
Log::channel('payments')->info('Payment processed', [
'transaction_id' => $transaction->id,
'amount' => $amount,
]);
Log::channel('security')->warning('Suspicious activity detected', [
'user_id' => $userId,
'ip' => $ip,
]);
Log::channel('api')->debug('External API call', [
'endpoint' => $endpoint,
'response_time' => $responseTime,
]);
Debugging Tools
Laravel provides helpful debugging functions:
<?php
// dd() - Dump and die (stops execution)
dd($user); // Shows formatted output and stops
// dump() - Dump without stopping
dump($user); // Shows output and continues
// ddd() - Dump, die, and debug (Laravel 9.3+)
ddd($user); // Enhanced dd() with more details
// ray() - Debug with Ray (requires ray package)
ray($user); // Sends to Ray desktop app
// Dump to log instead of browser
Log::debug('User data', ['user' => $user]);
// Dump SQL queries
DB::enableQueryLog();
// Execute queries
$queries = DB::getQueryLog();
dd($queries);
// Debug in Blade
@dump($variable)
@dd($variable)
Exception Reporting with External Services
Integrate with error tracking services for production monitoring:
# Install Sentry (popular error tracking service)
composer require sentry/sentry-laravel
# Publish configuration
php artisan vendor:publish --provider="Sentry\Laravel\ServiceProvider"
# Configure in .env
SENTRY_LARAVEL_DSN=https://your-dsn@sentry.io/project-id
<?php
// app/Exceptions/Handler.php
public function register(): void
{
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
Security Warning: Never log sensitive information like passwords, credit card numbers, or API keys. Use the $dontFlash property in the exception handler to prevent sensitive data from being logged.
Exercise 1: Custom Exception Class
Create a custom exception for insufficient inventory scenarios.
- Run
php artisan make:exception InsufficientInventoryException
- Add properties: productId, requestedQuantity, availableQuantity
- Implement custom constructor accepting these properties
- Implement report() method to log to custom "inventory" channel
- Implement render() method returning JSON for API or redirect for web
- Throw exception in OrderController when stock is insufficient
- Test with low-stock products
Exercise 2: Centralized Error Logging
Set up comprehensive error logging with custom channels.
- Create custom log channels: "errors", "security", "performance"
- Configure each channel with daily rotation and retention
- Create middleware to log all 4xx and 5xx HTTP responses
- Log authentication failures to security channel
- Log slow database queries (>1000ms) to performance channel
- Add contextual information (user, IP, URL) to all logs
- Test by triggering various errors and checking log files
Exercise 3: Custom 404 Page with Suggestions
Create an enhanced 404 error page with helpful suggestions.
- Create resources/views/errors/404.blade.php
- Display the requested URL that wasn't found
- Show 5 popular pages (most visited) as suggestions
- Add search box to help users find what they need
- Log 404 errors to track broken links
- Include breadcrumb navigation back to home
- Test by visiting non-existent URLs
Best Practices
- Use appropriate log levels (don't log everything as error)
- Add context to log messages for easier debugging
- Never log sensitive data (passwords, tokens, credit cards)
- Use daily log rotation to prevent huge log files
- Set up log retention policies to auto-delete old logs
- Create custom exceptions for domain-specific errors
- Provide user-friendly error messages (hide technical details)
- Use error tracking services (Sentry, Bugsnag) in production
- Monitor logs regularly and set up alerts for critical errors
- Test error handling paths (not just happy paths)
Summary
In this lesson, you learned:
- How Laravel's exception handling system works
- Throwing exceptions with abort() and custom classes
- Creating and using custom exceptions
- Catching and handling exceptions gracefully
- Creating custom error pages for HTTP status codes
- Laravel's logging system and Monolog integration
- Writing log messages at different severity levels
- Adding context to logs for better debugging
- Creating custom log channels for specific purposes
- Using debugging tools (dd, dump, ray)
- Integrating with external error tracking services
- Best practices for error handling and logging
Congratulations! You've completed the Laravel Framework fundamentals. Continue practicing by building real projects and exploring advanced topics like queues, broadcasting, and testing.