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();
$exceptions->render(...)
— We register a "handler." It says: "If an exception of typeModelNotFoundException
occurs, execute this code."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.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:
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);
}
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();
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
🚀 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:
- Create a custom exception like
PlanetNotFoundException
- Add handling for 404 errors in the
.withExceptions
closure- 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