Skip to content

Chapter 2.7: Error Handling

Time to learn: 40 minutes


1. Why Are Default Errors Bad?

If an error occurs in your Laravel application (for example, a record is not found in the database) and you haven't handled it, the user will see a huge HTML page with debug information or an uninformative "Server Error" message.

For an API, this is a disaster. Your frontend application expects to receive JSON, not HTML. Our task is to catch any error and turn it into a structured JSON response.


2. The Central Error Dispatcher: bootstrap/app.php

In older versions of Laravel, there was a cumbersome App\Exceptions\Handler.php file. In Laravel 11/12, everything has become much simpler and more elegant. The error handling control center is now located directly in your application's configuration file — bootstrap/app.php.

Open bootstrap/app.php. At the very bottom, you will see the .withExceptions(...) block. This is our "central dispatcher."

<?php
// bootstrap/app.php

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // ...
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // <-- THIS IS WHERE WE WILL WORK
    })->create();

3. Handling the Most Common Error: "Not Found" (404)

The most common error in an API is when a user requests a resource that does not exist (e.g., GET /api/planets/999). In this case, Laravel generates a ModelNotFoundException or NotFoundHttpException. Let's catch them.

Add the following code inside .withExceptions(...):

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // Catch the exception when a model is not found in the database
    $exceptions->render(function (ModelNotFoundException $e, Request $request) {
        // Check that the request is for our API
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'The requested resource was not found in our galaxy.'
            ], 404);
        }
    });

    // Catch the exception when the route itself is not found
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'This cosmic route does not exist.'
            ], 404);
        }
    });

})->create();
What did we do?

  1. $exceptions->render(...) — We register a "handler." It says: "If an exception of type ModelNotFoundException occurs, execute this code."
  2. if ($request->is('api/*')) — This is an important check. It ensures that our nice JSON response is only sent for API requests, without affecting regular web pages.
  3. return response()->json(...) — We create and return a standardized JSON response with a 404 code.

Now, if you request a non-existent planet, you will get a neat JSON response instead of an ugly HTML page.


4. Custom Exceptions: Creating Your Own "Alarm Signals"

Sometimes, standard exceptions are not enough. Imagine we have a business rule: "you cannot delete the planet 'Earth'". If someone tries to do this, we must return a meaningful error.

Step 1: Create our own exception class Run in the terminal:

php artisan make:exception CannotDeleteEarthException

Step 2: Use it in the controller Open PlanetController.php and modify the destroy method:

<?php
// app/Http/Controllers/PlanetController.php
use App\Exceptions\CannotDeleteEarthException; // <-- Import our exception
use App\Models\Planet;

public function destroy(Planet $planet)
{
    // Our new business rule
    if (strtolower($planet->name) === 'earth') {
        throw new CannotDeleteEarthException('Deleting planet Earth is forbidden by the Galactic Code.');
    }

    $planet->delete();
    return response()->json(null, 204);
}
Now, if someone tries to execute DELETE /api/planets/1 (where 1 is the ID of Earth), our code will throw a CannotDeleteEarthException.

Step 3: Teach Laravel to handle our "alarm" gracefully Let's go back to bootstrap/app.php and add a new handler for our exception.

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // Our new handler
    $exceptions->render(function (CannotDeleteEarthException $e, Request $request) {
        return response()->json([
            'message' => 'Operation forbidden.',
            'details' => $e->getMessage() // Get the message we passed in the throw
        ], 403); // 403 Forbidden
    });

    // ... (other handlers for 404)

})->create();
Done! We have created our own named exception, which makes the controller code cleaner, and taught Laravel to turn it into a beautiful, meaningful JSON response with the correct HTTP status.


5. Handling All Other Failures (500 Internal Server Error)

What about all other, unforeseen errors? For example, if the database connection is lost or there is a syntax error in the code. For this, we can register a "universal" handler for the most general type of error — Throwable.

Important: This handler must be the last one so that it does not catch the more specific exceptions we defined above.

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // ... (handlers for CannotDeleteEarthException and 404)

    // UNIVERSAL HANDLER (at the very end)
    $exceptions->render(function (Throwable $e, Request $request) {
        if ($request->is('api/*')) {
            // In debug mode, you can show the real error message
            $message = config('app.debug')
                ? 'An error occurred: ' . $e->getMessage()
                : 'An unexpected error occurred on board. Engineers have been dispatched.';

            return response()->json(['message' => $message], 500);
        }
    });

})->create();

Now any "unknown" exception will be neatly caught and turned into a JSON response with a 500 code, without breaking your API or showing the user unnecessary information.


6. Error Logging: The Spaceship's Black Box

Logging settings in config/logging.php:

<?php
'channels' => [
    'space_api' => [
        'driver' => 'daily',
        'path' => storage_path('logs/space_api.log'),
        'level' => 'error',
        'days' => 14,
    ],
],

Adding a log entry:

<?php
try {
    // Code with risk of error
} catch (Exception $e) {
    Log::channel('space_api')->error('Error accessing planets', [
        'exception' => $e,
        'request' => request()->all(),
        'user_id' => auth()->id()
    ]);
    throw $e;
}


Review Quiz

1. The HTTP status for "Planet not found" is:

2. In modern Laravel, where is global error handling configured?

3. The command to create a custom exception is:

4. To create a separate log channel for API errors, you should:

5. The main advantage of creating custom exceptions is:


🚀 Chapter Summary:

You have equipped your API with a reliable rescue system:

  • 🛟 Global interception of standard errors
  • 🪐 Custom exceptions with clear codes
  • 📝 A unified JSON format for all errors
  • 🔍 Logging with incident details

The spaceship is ready for emergencies! In the final chapter of this section, we will test all the systems.

📌 Checkpoint:

  1. Create a custom exception like PlanetNotFoundException
  2. Add handling for 404 errors in the .withExceptions closure
  3. Test a request to a non-existent planet

⚠️ If errors are not being caught:

  • Make sure is('api/*') matches your routes
  • Check the order of your handlers within withExceptions
  • For custom exceptions, make sure you are using throw new