Courses

Handling Exceptions and Errors in Laravel 12

Handling Exceptions in Laravel API

Working with API often means that "lazy" front-end developers show the error messages on the page/screen exactly as you return them from the back end. And in Laravel, some automatic Exception messages are not that informative:

Let's see how we can improve this with quick fixes.


Handling Exceptions in API

Handling exceptions in the API is not that different from a typical Laravel web project. The only problem is Laravel will automatically return the Exception message, which may not only be confusing but also result in a potential data leak:

A few things are wrong here:

  • This message is not user-friendly and should be visible only to the developer
  • This message returns the ID you were trying to access. It is a potential data leak
  • This message returns the Model class with the namespace. It could be treated as a data leak

The data leaks above may be a security issue, as someone trying to call our API maliciously would find out the structure of our IDs or namespaces.

So, how can we fix this? Well, one of the documented ways is to use a different render method, with $exceptions->render():

Controller

use App\Models\User;
use Illuminate\Http\Request;
 
// ...
 
public function __invoke(Request $request)
{
$user = User::findOrFail($request->input('user_id'));
 
// ...
}

bootstrap/app.php:

use Illuminate\Http\Request;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->wantsJson()) {
return response()->json([
'message' => 'Resource not found',
], 404);
}
});
})->create();

This code will allow us to handle the exceptions and return a user-friendly message globally:

Yet, this comes with another issue - this will handle all of our 404 responses. For example, if we try to access an endpoint route that does not exist, we will get the same message:

This flow is pretty bad and can produce unwanted results later on.


Using try-catch with Exceptions

In this case, I suggest not relying on the general Laravel Exception Handler but trying to catch a specific Exception ourselves. We're looking specifically for ModelNotFoundException here.

Controller

use App\Models\User;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
 
// ...
 
public function __invoke(Request $request)
{
try {
$user = User::findOrFail($request->input('user_id'));
 
// ...
} catch (ModelNotFoundException $modelNotFoundException) {
return response()->json([
'message' => 'User not found'
], 404);
} catch (Exception $exception) {
return response()->json([
'message' => 'Something went wrong'
], 500);
}
}

This code will allow us to handle different Exceptions and return a user-friendly message. Each type of Exception here can get its message and status code.

As you can see, we have set a different message in case we don't find the user. Yet, if anything else fails - we will return a generic message. And this is very expandable!

public function __invoke(Request $request)
{
try {
$user = User::findOrFail($request->input('user_id'));
 
// ...
} catch (ModelNotFoundException $modelNotFoundException) {
return response()->json([
'message' => 'User not found'
], 404);
} catch (IncompleteDataException $incompleteDataException) {
return response()->json([
'message' => 'User has incomplete data'
], 500);
} catch (NoAccessException $noAccessException) {
return response()->json([
'message' => 'Current user has no access to this user'
], 403);
} catch (Exception $exception) {
return response()->json([
'message' => 'Something went wrong'
], 500);
}
}

Of course, it introduces more code in Controller, but it's a lot more flexible and allows us better control over what API returns.


So, that's it for this course. Good luck handling errors and exceptions in your Laravel projects!

avatar

Thank you for this course

avatar
You can use Markdown
avatar

Thanks for the course, but I missed the theory about the reasonable use of exceptions. Now it looks to me that at least 50% of almost any code can be surrounded by try/catch constructs and create dozens or even hundreds of exception classes. I understand that this is all very individual, but maybe you can tell a few words from personal experience how you determine when to use exceptions.

avatar

While it's individual, for me it always comes down to:

What will happen if there's an issue here? Will the flow break? Do I handle it correctly?

And once that is clear - you either add an exception or improve validation of the data. For example, imports are a great place to add exceptions, to prevent complete failure. Same goes for any public facing API - exceptions are a really nice way to handle situations where someone misused something (sent wrong input or just doesn't have the correct data inside). In any case, I wouldn't go too deep to handle everything, as some things will be handled by framework in a nice way (yet, it is important to have a bug tracker with it!). If it isn't handled - I'd see importance of this working and returning an output with a friendly message

avatar

Thanks for the detailed answer! Exceptions are also important, for example, in the processing of requests to payment systems.

avatar

Definitely! But I didn't mention it because that feels self-explanatory :)

avatar

great tutorial!

avatar
You can use Markdown
avatar

Helloo thanks for this course. Having a question. Base on this above code example should I base all my database relative operation, like Eloquent retrieving and fetching (as you did for user) inside a try-catch ?

I no, when do your really recommand to use it ?

avatar

I would not cover retrieving operations with it, unless it's crucial. But I would cover majority of insert/update ones!

avatar
You can use Markdown
avatar
Loganathan Natarajan

Useful course. Thanks

avatar
You can use Markdown
avatar

Hi , what if user type like this http://api.test.com/api/v1/users/3dd ? how to catch this error? I tried adding multiple exceptions but even Exception class does not catch it. can somebody help ?

avatar

Can you write an error message you get in this case?

avatar

App\Services\Api\V1\UserService::getUserById(): Argument #1 ($id) must be of type int, string given,

avatar

In this case, the error is from types/php. So better solution would be to add regex validation on route params

avatar

this what in the api.php

Route::apiresource('/users', UserController::class);

in this case should i have to specifiy each route and add the regex to one route or is there any other way

thanks

avatar

Sadly - yea. I don't remember seeing a better way to do this (unless running a middleware that would check if it's okay). But that complicates things.

avatar
You can use Markdown
avatar
You can use Markdown