Laravel Framework

Error Handling & Logging in Laravel

15 min Lesson 15 of 45

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.

  1. Run php artisan make:exception InsufficientInventoryException
  2. Add properties: productId, requestedQuantity, availableQuantity
  3. Implement custom constructor accepting these properties
  4. Implement report() method to log to custom "inventory" channel
  5. Implement render() method returning JSON for API or redirect for web
  6. Throw exception in OrderController when stock is insufficient
  7. Test with low-stock products

Exercise 2: Centralized Error Logging

Set up comprehensive error logging with custom channels.

  1. Create custom log channels: "errors", "security", "performance"
  2. Configure each channel with daily rotation and retention
  3. Create middleware to log all 4xx and 5xx HTTP responses
  4. Log authentication failures to security channel
  5. Log slow database queries (>1000ms) to performance channel
  6. Add contextual information (user, IP, URL) to all logs
  7. 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.

  1. Create resources/views/errors/404.blade.php
  2. Display the requested URL that wasn't found
  3. Show 5 popular pages (most visited) as suggestions
  4. Add search box to help users find what they need
  5. Log 404 errors to track broken links
  6. Include breadcrumb navigation back to home
  7. 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.