Back to Scaling PHP Applications guides

Laravel Error Handling Patterns

Stanley Ulili
Updated on June 23, 2025

Good error handling makes your Laravel app rock-solid. When something breaks, you want your users to see helpful messages instead of scary technical errors. You want your app to keep working even when things go wrong.

Bad error handling crashes your app, shows users confusing messages, and makes debugging a nightmare. You'll spend hours trying to figure out what went wrong and where.

This guide shows you how to handle errors properly in Laravel. You'll learn patterns that keep your app running smoothly and provide users with clear feedback when problems arise.

Understanding Laravel's exception landscape

Laravel apps face different types of errors. You need to know what kinds of problems can happen before you can fix them properly.

Expected operational failures

These errors happen during normal use. You should plan for them because they're part of how your app works, not bugs in your code.

Here's what you'll see in most Laravel apps:

  • Database connections fail when external services go down
  • Validation fails when users enter bad data
  • Users try to log in with wrong passwords
  • File uploads fail when files are too big or the wrong type
  • API calls hit rate limits
  • Users try to access records that don't exist

Programming defects and logic errors

These are actual bugs in your code. You need to fix them, not just handle them at runtime.

Common examples include trying to use undefined array keys in Blade templates, which breaks your pages. Setting up Eloquent relationships wrong causes SQL errors. Messing up service providers breaks dependency injection when your app starts.

You might also see type errors when PHP expects one data type but gets another. Using Laravel collections incorrectly throws exceptions when methods expect data that isn't there.

Framework constraint violations

Laravel has rules. When you break them, you get specific errors that need special handling.

Route model binding fails when URL parameters don't match database records, automatically returning 404s. Middleware can stop requests when authorization fails. Queue jobs need different error handling than web requests.

Artisan commands need console-specific error handling, not HTTP responses. Event listeners can fail silently and break functionality without obvious symptoms.

Once you understand these error types, you can handle each one properly.

Global exception handling architecture

Laravel handles all unhandled exceptions through your App\Exceptions\Handler class. This is your control center for deciding how to log, report, and display different types of errors.

Exception handler customization

The exception handler has several methods that control different parts of error processing. You customize these methods to manage exceptions the way you want:

 
// app/Exceptions/Handler.php
class Handler extends ExceptionHandler
{
    public function register()
    {
        $this->reportable(function (Throwable $e) {
            if ($e instanceof ModelNotFoundException) {
                logger()->warning('Model not found', [
                    'model' => $e->getModel(),
                    'url' => request()->url(),
                ]);
            }
        });

        $this->renderable(function (ModelNotFoundException $e, $request) {
            if ($request->expectsJson()) {
                return response()->json([
                    'error' => 'Resource not found'
                ], 404);
            }

            return response()->view('errors.model-not-found', [], 404);
        });
    }
}

The register method lets you define how to handle specific exceptions without cluttering up your main handler methods. The reportable method controls what gets logged and how. The renderable method decides what response format to use for different exception types.

This approach gives you rich logging for debugging while showing user-friendly error messages. The handler can tell the difference between API requests and web requests, returning the right response format for each.

Context-aware exception responses

Laravel apps often serve both web pages and API endpoints. You need different response strategies for the same errors:

 
// app/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
    if ($request->expectsJson()) {
        return $this->handleApiException($exception);
    }

    return $this->handleWebException($exception);
}

private function handleApiException(Throwable $exception)
{
    $statusCode = $exception instanceof HttpException 
        ? $exception->getStatusCode() 
        : 500;

    return response()->json([
        'error' => true,
        'message' => $exception->getMessage() ?: 'An error occurred',
        'status_code' => $statusCode
    ], $statusCode);
}

This pattern gives you consistent error responses across different client types while providing the right amount of detail. API clients get structured JSON with optional debugging info. Web clients get redirected to proper error pages with preserved form data.

Separating the logic keeps your rendering code organized. You can modify behavior for specific request types without affecting others.

Model and database exception patterns

Database operations are common sources of exceptions in Laravel apps. You need solid patterns around these operations to prevent data problems and give meaningful feedback when database operations fail.

Eloquent exception handling

Model operations can fail in many ways, from validation errors to database constraint violations. You need consistent patterns around these failures to make your app more reliable:

 
// app/Services/UserService.php
class UserService
{
    public function createUser(array $userData)
    {
        try {
            $validated = validator($userData, [
                'email' => 'required|email|unique:users',
                'name' => 'required|string|max:255',
            ])->validate();

            return User::create($validated);

        } catch (QueryException $e) {
            if ($e->errorInfo[1] === 1062) { // Duplicate entry
                throw new UserCreationException('Email already exists');
            }

            logger()->error('User creation failed', ['error' => $e->getMessage()]);
            throw new UserCreationException('Failed to create user');
        }
    }
}

This service layer approach wraps database exception handling while providing meaningful error messages. It tells the difference between types of query failures and provides context-specific error messages. The service logs detailed information for debugging while exposing user-friendly exceptions to your application.

Custom exceptions like UserCreationException give you a clear interface for handling specific failure scenarios while keeping the original exception context for debugging.

Transaction rollback strategies

Database transactions need careful exception handling to keep your data consistent. Laravel gives you several approaches for managing transactional operations:

 
// app/Services/OrderService.php
class OrderService
{
    public function processOrder(array $orderData)
    {
        return DB::transaction(function () use ($orderData) {
            try {
                $order = Order::create($orderData);
                $this->updateInventory($orderData['items']);
                $this->processPayment($order);

                return $order;

            } catch (\Exception $e) {
                logger()->error('Order processing failed', [
                    'error' => $e->getMessage()
                ]);

                throw new OrderProcessingException(
                    'Unable to process order. Please try again.'
                );
            }
        });
    }
}

Laravel's transaction wrapper automatically rolls back all database changes when any exception happens inside the transaction closure. This keeps your data consistent even when complex multi-step operations fail partway through.

The service method provides a clean interface while handling all the complexity of coordinating multiple database operations. Custom exceptions give meaningful error messages to users while preserving technical details for debugging.

HTTP exception handling patterns

Web applications need specific patterns for handling HTTP-related exceptions, from routing failures to middleware rejections. Laravel provides several ways to manage these scenarios gracefully.

Route-specific exception handling

Different routes might need different exception handling strategies based on their purpose and expected user interactions:

 
// app/Http/Controllers/Api/ProductController.php
class ProductController extends Controller
{
    public function show($id)
    {
        try {
            $product = Product::findOrFail($id);

            return response()->json([
                'success' => true,
                'data' => $product
            ]);

        } catch (ModelNotFoundException $e) {
            return response()->json([
                'success' => false,
                'error' => 'Product not found'
            ], 404);
        }
    }
}

This pattern gives you consistent API responses while handling different exception types properly. Each endpoint handles its specific failure scenarios while keeping a uniform response structure that clients can reliably parse.

The validation exception handling preserves field-specific error messages. This lets frontend applications give targeted feedback to users about input problems.

Middleware exception handling

Middleware components can catch and handle exceptions before they reach your application logic. This gives you centralized exception management for cross-cutting concerns:

 
// app/Http/Middleware/ApiExceptionHandler.php
class ApiExceptionHandler
{
    public function handle(Request $request, Closure $next)
    {
        try {
            return $next($request);
        } catch (HttpException $e) {
            return response()->json([
                'success' => false,
                'error' => $e->getMessage(),
                'status_code' => $e->getStatusCode()
            ], $e->getStatusCode());
        } catch (\Exception $e) {
            logger()->error('API exception', ['error' => $e->getMessage()]);

            return response()->json([
                'success' => false,
                'error' => 'An unexpected error occurred'
            ], 500);
        }
    }
}

Middleware exception handling gives you a safety net for your API endpoints. You get consistent error responses even when controllers don't handle specific exception types. This approach reduces code duplication across controllers while keeping centralized logging and error reporting.

The middleware can distinguish between different exception types and provide tailored responses while maintaining detailed logging for debugging purposes.

Validation and form exception patterns

Form validation creates one of the most common sources of exceptions in web applications. Laravel's validation system gives you comprehensive tools for handling these scenarios gracefully.

Custom validation exception handling

Beyond Laravel's built-in validation, apps often need custom validation logic that can generate specific exception types:

 
// app/Http/Requests/CreateUserRequest.php
class CreateUserRequest extends FormRequest
{
    public function rules()
    {
        return [
            'email' => 'required|email|unique:users',
            'name' => 'required|string|max:255',
            'password' => 'required|string|min:8|confirmed',
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        if ($this->expectsJson()) {
            throw new HttpResponseException(
                response()->json([
                    'success' => false,
                    'error' => 'Validation failed',
                    'details' => $validator->errors()
                ], 422)
            );
        }

        parent::failedValidation($validator);
    }
}

This form request class shows comprehensive validation exception handling. It provides custom error messages that are user-friendly while implementing complex validation logic. The failedValidation override ensures consistent JSON responses for API requests while keeping standard web behavior for form submissions.

You can wrap custom validation logic within the form request. This keeps controllers clean while providing rich validation feedback to users.

Bulk operation exception handling

When you process multiple records, exception handling gets more complex. Partial failures need different strategies:

 
// app/Services/BulkUserService.php
class BulkUserService
{
    public function createUsers(array $usersData)
    {
        $results = ['successful' => [], 'failed' => [], 'errors' => []];

        foreach ($usersData as $index => $userData) {
            try {
                $user = User::create($userData);
                $results['successful'][] = ['index' => $index, 'user' => $user];

            } catch (\Exception $e) {
                $results['failed'][] = $index;
                $results['errors'][$index] = $e->getMessage();
            }
        }

        if (empty($results['successful'])) {
            throw new BulkOperationException('All user creation failed', $results);
        }

        return $results;
    }
}

This bulk operation service gives you detailed error handling that allows partial success scenarios. It collects both successful operations and failures, providing detailed feedback about which records processed successfully and which had problems.

The service wraps the entire operation in a transaction, ensuring data consistency while still providing detailed error reporting for individual record failures.

Logging and monitoring patterns

Good exception handling needs comprehensive logging and monitoring. You need to identify patterns, track error rates, and enable quick debugging of production issues.

Structured exception logging

Laravel's logging system gives you powerful tools for creating structured exception logs that make debugging and monitoring easier:

 
// app/Exceptions/Handler.php
private function logException(Throwable $exception)
{
    $context = [
        'exception' => get_class($exception),
        'message' => $exception->getMessage(),
        'file' => $exception->getFile(),
        'line' => $exception->getLine(),
        'trace_id' => Str::uuid()->toString(),
    ];

    if (request()) {
        $context['request'] = [
            'url' => request()->url(),
            'method' => request()->method(),
            'ip' => request()->ip(),
        ];
    }

    if (auth()->check()) {
        $context['user'] = ['id' => auth()->id()];
    }

    Log::error('Exception occurred', $context);
}

This logging approach creates structured exception records that include comprehensive context while protecting sensitive information. Each exception gets a unique trace ID that you can use to connect logs across different systems and requests.

The context includes request information, user details, and environmental data that helps with debugging while making sure sensitive data like passwords get removed from logs.

Exception rate monitoring

Monitoring exception rates and patterns helps you identify systemic issues before they significantly impact users:

 
// app/Services/ExceptionMonitoringService.php
class ExceptionMonitoringService
{
    private const RATE_LIMIT_WINDOW = 300; // 5 minutes
    private const ALERT_THRESHOLD = 10;

    public function recordException(string $exceptionType, array $context = [])
    {
        $key = "exception_count:{$exceptionType}:" . floor(time() / self::RATE_LIMIT_WINDOW);
        $count = Cache::increment($key);

        Cache::expire($key, self::RATE_LIMIT_WINDOW);

        if ($count === self::ALERT_THRESHOLD) {
            Log::alert('High exception rate detected', [
                'exception_type' => $exceptionType,
                'count' => $count,
                'time_window' => self::RATE_LIMIT_WINDOW,
            ]);
        }
    }
}

This monitoring service tracks exception patterns and triggers alerts when error rates go above normal thresholds. It gives you early warning of systemic issues while maintaining performance through efficient caching.

You can integrate the service with external monitoring systems to get comprehensive visibility into application health and error patterns.

Performance and debugging considerations

Exception handling mechanisms can impact application performance, especially in high-traffic scenarios. Understanding these implications helps you optimize exception handling strategies.

Exception performance optimization

Frequent exception handling can create performance bottlenecks. You can implement efficient patterns that reduce overhead while keeping comprehensive error coverage:

 
// app/Services/OptimizedDataService.php
class OptimizedDataService
{
    public function getUserData(int $userId)
    {
        // Use existence checks instead of exception-based flow control
        if (!$this->userExists($userId)) {
            return null;
        }

        return Cache::remember("user_data:{$userId}", 3600, function () use ($userId) {
            return User::find($userId);
        });
    }

    private function userExists(int $userId): bool
    {
        return Cache::remember("user_exists:{$userId}", 1800, function () use ($userId) {
            return User::where('id', $userId)->exists();
        });
    }
}

This optimization approach avoids using exceptions for flow control, which can be expensive performance-wise. Instead, it uses existence checks and early returns to handle missing data scenarios efficiently.

Caching strategies reduce database queries while keeping data fresh. Batch operations minimize individual exception handling overhead.

Development debugging patterns

Exception handling in development environments needs different strategies than production. You want to help debugging while keeping code quality:

 
// app/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
    if (app()->environment('local')) {
        return $this->renderDevelopmentException($request, $exception);
    }

    return $this->renderProductionException($request, $exception);
}

private function renderDevelopmentException($request, Throwable $exception)
{
    if ($request->expectsJson()) {
        return response()->json([
            'error' => true,
            'message' => $exception->getMessage(),
            'exception' => get_class($exception),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
        ], 500);
    }

    return parent::render($request, $exception);
}

This development-focused exception handling gives you comprehensive debugging information while ensuring production security. Development environments get detailed exception information including SQL queries and request data. Production environments show minimal, user-friendly error messages.

This separation lets you debug efficiently during development while keeping security and user experience in production deployments.

Final thoughts

Good exception handling turns potential app failures into opportunities for graceful degradation and better user experience. When you implement these patterns thoughtfully, your Laravel app remains stable under adverse conditions while providing meaningful feedback that helps users complete their tasks.

The key is understanding that exceptions aren't just technical events to log and ignore. They're opportunities to communicate with your users.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github