Laravel for Beginners: Error Handling

Arlind Musliu cofounder at Lucky Media
Arlind Musliu

January 17, 2024 · 6 min read

Laravel for Beginners: Error Handling

2026 UPDATE - LARAVEL 13

We’re excited to announce that we have updated all of our blog post examples to reflect the new Laravel 13 version! Our previous examples were based on Laravel 10, but with the release of Laravel 13, we wanted to ensure that our readers have access to the most up-to-date information and examples.

Enhanced UX with better Error Handling

Every developer knows that encountering errors is an inevitable part of building an application. However, how you handle these errors can make a significant difference in the user experience. In Laravel, error handling is built to be simple yet robust, providing you with tools to manage exceptions gracefully.

Understanding Laravel’s Error Handling

Laravel 13 automatically configures error and exception handling for new projects. You can customize this by using the withExceptions method in bootstrap/app.php, which allows you to control how exceptions are reported and rendered. The $exceptions object, an instance of Illuminate\Foundation\Configuration\Exceptions, manages this process.

The Report Method

The report method is used to log exceptions or send them to an external service like Sentry. By default, Laravel logs errors in the storage/logs directory. If you need to customize the logging behavior, you can modify this method.

The Render Method

The render method is responsible for converting exceptions into HTTP responses that are sent back to the browser. Laravel includes a set of predefined exceptions that result in the corresponding HTTP status codes, such as NotFoundHttpException for 404 errors.

Customizing Error Pages

Laravel allows you to easily customize error pages for different HTTP status codes. For example, if you want to create a custom 404 Not Found page, you can create a 404.blade.php file in the resources/views/errors directory.

@extends('layouts.app')

@section('title', 'Page Not Found')

@section('content')
    <h1>Oops! The page you are looking for can't be found.</h1>
    <p>Return to the <a href="{{ url('/') }}">homepage</a>.</p>
@endsection

With this custom view, users will see a friendly error message instead of the default error page when they try to access a non-existent route on your blog.

Laravel for Beginners: Form Requests and Validation Rules

Handling Form Validation Errors

Laravel makes handling form validation errors straightforward. When using form requests, as we explained in the article Form Requests and Validation Rules, Laravel automatically redirects the user back to the form with error messages if the validation fails.

In your Blade templates, you can display these errors using the $errors variable provided by Laravel:

@if ($errors->any())
    <div>
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

Logging Exceptions in the Controller

In addition to displaying errors, you may want to log them for debugging purposes. Laravel allows you to configure logging channels in config/logging.php. You can log errors to files, the system error log, external services, and more.

Within your PostController, you might encounter situations where you need to log an error, for instance, when an exception occurs during the creation or updating of a blog post. Laravel provides a simple way to log errors through the Log facade.

Here’s an example of how you could log an error in the store method of the PostController:

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use Illuminate\Support\Facades\Log;

class PostController extends Controller
{
    public function store(StorePostRequest $request)
    {
        try {
            $validatedData = $request->validated();
            
            $post = Post::create($validatedData);
            
            // other stuff
            
            // Redirect to the new post with a success message
            return redirect()->route('posts.show', $post)
							 ->with('success', 'Post created successfully!');
        } catch (\Exception $e) {
            
			// Log the error
            Log::error('Post creation failed', [
                'error' => $e->getMessage(),
                'user_id' => $request->user()->id,
            ]);

            // Redirect back with an error message
            return back()->withInput()
						 ->withErrors(['msg' => 'Post creation failed']);
        }
    }
}

In this example, we wrap our post-creation logic in a try-catch block. If an exception occurs while attempting to create a new post, we catch it and use the Log facade to record the error. The Log::error method takes two parameters: the error message and an array of contextual data that can help with debugging.

The contextual data array includes the exception message and any other relevant information, such as the user’s ID. This data is beneficial when reviewing the logs, as it provides more insight into what might have caused the error.

After logging the error, we redirect the user back to the previous page with the input data they provided (back()->withInput()) and include an error message to inform them that the post creation failed.

Remember to import the Log facade at the top of your controller file.

laravel best framework php

Creating Custom Exception Classes

For domain-specific errors, create custom exception classes rather than catching generic \Exception everywhere. Laravel generates them with Artisan:

php artisan make:exception PostNotFoundException

This creates app/Exceptions/PostNotFoundException.php. You can define how the exception is rendered directly on the class — no need to register it in bootstrap/app.php:

&lt;?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class PostNotFoundException extends Exception
{
    // Customize the exception message
    public function __construct(int $id)
    {
        parent::__construct("Post with ID {$id} was not found.");
    }

    // Control how this exception is rendered (returned to browser/API client)
    public function render(Request $request): Response
    {
        if ($request->expectsJson()) {
            return response()->json([
                'error' => 'Post not found',
                'message' => $this->getMessage(),
            ], 404);
        }

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

    // Control how this exception is logged/reported
    public function report(): void
    {
        // Only log if it's unexpected (e.g. direct URL manipulation)
        logger()->warning($this->getMessage());
    }
}

Then throw it from your controller:

public function show(int $id): View
{
    $post = Post::find($id);

    if (!$post) {
        throw new PostNotFoundException($id);
    }

    return view('posts.show', compact('post'));
}

Abort Helpers: The Quickest Way to Throw HTTP Errors

For simple HTTP error cases you do not need a full custom exception. Laravel’s abort helpers let you throw HTTP exceptions in one line:

// Throw a 404 immediately
abort(404);

// Throw 403 with a custom message
abort(403, 'You are not authorized to view this post.');

// Throw 404 only if a condition is true
abort_if(!$post->isPublished(), 404);

// Throw 403 unless a condition is true
abort_unless(auth()->check(), 403, 'Login required.');

// Practical example: only the post author can edit it
abort_unless(auth()->id() === $post->user_id, 403);

These helpers throw an HttpException which Laravel renders using your custom error page for that status code (e.g. resources/views/errors/403.blade.php).

Handling ModelNotFoundException from Route Model Binding

When you use route model binding (type-hinting a model in your controller method), Laravel automatically queries the database. If no record is found, it throws a ModelNotFoundException which results in a 404 response by default.

// Route definition
Route::get('/posts/{post}', [PostController::class, 'show']);

// Controller - Laravel finds the Post or throws ModelNotFoundException
public function show(Post $post): View
{
    return view('posts.show', compact('post'));
}

To customize the response when a model is not found, use withExceptions in bootstrap/app.php:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (ModelNotFoundException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'error' => 'Resource not found',
            ], 404);
        }
    });
})

Returning JSON Error Responses for API Routes

If your Laravel application serves API clients (mobile apps, SPAs, third-party services), HTML error pages are useless — they need structured JSON. Laravel automatically returns JSON for exceptions when the request has an Accept: application/json header. You can check this explicitly with $request->expectsJson():

->withExceptions(function (Exceptions $exceptions) {
    // Return JSON for all exceptions on API routes
    $exceptions->render(function (\Exception $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'error' => class_basename($e),
                'message' => $e->getMessage(),
            ], method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500);
        }
    });
})

Laravel’s ValidationException automatically returns a 422 response with a structured errors object for JSON requests — no extra configuration needed:

{
  "message": "The title field is required.",
  "errors": {
    "title": ["The title field is required."],
    "content": ["The content field is required."]
  }
}

Custom Error Pages for All HTTP Codes

Create a Blade file in resources/views/errors/ named after the HTTP status code. Laravel will use it automatically:

resources/views/errors/
├── 403.blade.php   # Forbidden
├── 404.blade.php   # Not Found
├── 419.blade.php   # Page Expired (CSRF token mismatch)
├── 500.blade.php   # Server Error
└── 503.blade.php   # Service Unavailable (maintenance mode)

Each view has access to the $exception variable. A simple 500 page:

@extends('layouts.app')

@section('title', 'Server Error')

@section('content')
    <div class="text-center py-20">
        <h1 class="text-4xl font-bold">500</h1>
        <p class="mt-4 text-gray-600">Something went wrong on our end. We are looking into it.</p>
        <a href="{{ url('/') }}" class="mt-6 inline-block text-blue-500 hover:underline">
            Go back home
        </a>
    </div>
@endsection

To test your custom error pages locally without deploying, set APP_DEBUG=false in your .env file and add a temporary route that triggers an error:

// Only for local testing - remove before deploying!
Route::get('/test-500', function () {
    abort(500);
});

Setting Up Sentry for Error Monitoring

The storage/logs directory works for local development, but in production you need real-time error monitoring. Sentry is the most widely used option and integrates directly with Laravel:

composer require sentry/sentry-laravel

php artisan sentry:publish --dsn=https://your-dsn@sentry.io/your-project-id

The publish command adds your DSN to .env and creates config/sentry.php. Laravel’s exception handler automatically sends unhandled exceptions to Sentry — no additional code needed. To also associate errors with the authenticated user:

// In bootstrap/app.php withExceptions callback:
$exceptions->report(function (\Exception $e) {
    if (auth()->check()) {
        \Sentry\configureScope(function (\Sentry\State\Scope $scope) {
            $scope->setUser([
                'id' => auth()->id(),
                'email' => auth()->user()->email,
            ]);
        });
    }
});

Sentry captures the full stack trace, breadcrumbs, request data, and environment details automatically. You can verify the integration is working by running:

php artisan sentry:test

Conclusion

Effective error handling is a vital part of any web application, and Laravel provides you with the tools to do it well. From custom exception classes that carry their own rendering logic, to abort helpers for quick HTTP errors, to structured JSON responses for API clients, to Sentry for production monitoring - Laravel’s error handling system covers every layer of your application.

Upcoming Articles in the Series

  1. Laravel for Beginners: Using Queues

  2. Laravel for Beginners: Sending Emails

  3. Laravel for Beginners: Localization and Languages

This article is part of our series Laravel for Beginners: A Step-by-Step Guide to Learn the Concepts.


Bring Your Ideas to Life 🚀

If you need help with a Laravel project let’s get in touch.

Lucky Media is proud to be recognized as a leading Laravel Development Agency

Technologies

Laravel
Arlind Musliu cofounder at Lucky Media
Arlind Musliu

Cofounder and CFO of Lucky Media

Stay up-to-date

Be updated with all news, products and tips we share!

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork