DEV Community

Jack Miras
Jack Miras

Posted on • Updated on

Laravel's exceptions: Part 3 – findOrFail exception automated

When implementing something that requires querying the database to find a record by its ID, the first thing that comes to mind is the find() function from Eloquent.

Even though the function works, and you will be able to retrieve your data when it exists, a null object will be returned if the record is not found, and this null object usually brings unnecessary complexity.

As you know, Laravel is a full-featured framework, but one of the features that blew my mind once I got the hang of it, was the exception handling of the framework. It’s not random that some of Laravel’s helpers throw exceptions instead of returning null objects, such as the findOrFail() function.

These helpers that throw exceptions can be pretty handy when you learn how to automate the handling of their exceptions, making the code cleaner and shorter. But before we dive into those functions or Laravel’s exception handler, let’s take a look at ways that a find operation can be implemented.

Content

Ways to implement

In the following two sections, we will see the most conventional ways of implementing a find operation. You will notice that the number of lines between the implementations is the same; although they are entirely different implementations and there are no real advantages between them, the choice of the approach is just a matter of preference.

The find function

Down below, we have a real use case where we are implementing a show action to the UsersController to find a user by its ID and render the record as a response to an endpoint.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class UsersController extends Controller
{
    public function show(Request $request): Response
    {
        $user = User::find($request->id);

        if ($user === null) {
            return response("User with id {$request->id} not found", Response::HTTP_NOT_FOUND);
        }

        return response($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

First, we query a user by its ID using the find function and store its result in an $user object.

Next, because the record of a given ID may not be found in the database, we are checking if the $user is null; in case it is, we will return a message that would evaluate to User with id 1 not found.

Finally, if the ID passed into the query got found, the user object would be present, and we could just return it as a response to the API call.

The findOrFail function

With the same use case as before, let’s try a second approach and see how our code looks when using the findOrFail() function without automating its exception handling.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class UsersController extends Controller
{
    public function show(Request $request): Response
    {
        try {
            $user = User::findOrFail($request->id);
        } catch (ModelNotFoundException $exception) {
            return response("User with id {$request->id} not found", Response::HTTP_NOT_FOUND);
        }

        return response($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

At the try{} block we are querying the user by its ID; when found, we will place the data at the $user, but unlike the previous approach, a ModelNotFoundException will be thrown in case the record doesn't exist.

Thereafter, the catch{} block gets used to intercept the exception thrown; once the exception gets intercepted, we build a response with a message that would be evaluated to User with id 1 not found.

Then, if the given ID gets found, the $user object would be present, and we would return it as a response to the endpoint.

Automating exceptions

It’s important to notice that the approach about to be explained can be used to automate the handling of various Laravel exceptions. By automating these exceptions, you will have cleaner code and a more structured way of handling the exceptions.

To automatically handle the exception thrown by the findOrFail() function, we will have to override the render() function in the app/Exceptions/Handler.php class, as shown in the example down below.

<?php

namespace App\Exceptions;

use Throwable;
use Illuminate\Support\Arr;
use Illuminate\Http\Response;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Throwable  $e
     * @return \Symfony\Component\HttpFoundation\Response
     *
     * @throws \Throwable
     */
    public function render($request, Throwable $e)
    {
        if ($e instanceof ModelNotFoundException) {
            $replacement = [
                'id' => collect($e->getIds())->first(),
                'model' => Arr::last(explode('\\', $e->getModel())),
            ];

            $error = new Error(
                help: trans('exception.model_not_found.help'),
                error: trans('exception.model_not_found.error', $replacement)
            );

            return response($error->toArray(), Response::HTTP_NOT_FOUND);
        }

        return parent::render($request, $e);
    }
}
Enter fullscreen mode Exit fullscreen mode

We start by declaring a conditional that checks if the $e being received in the render() parameter is an instance of the ModelNotFoundException thrown by the findOrFail() used in the previous examples.

Then, we create a $replacement array, an associative with the keys id and model, whose values are getting extracted from the intercepted exception. This replacement array gets used as a replacement parameter in a trans() function that gets called in the constructor of the error object.

Subsequently, we are creating an $error object. In case you don’t know what this Error class is about, please take a moment to read the previous post in this series, where I discuss custom exceptions. The error class takes two mandatory parameters: $help which shows a possible solution to the error, and $error where the error that happened gets specified.

Note that we are using named parameters and the trans() helper to specify the messages we want to pass into the $help and $error parameters from the error object.

Lastly, with the error object instantiated, we can build the response object to be rendered to the API call. To achieve that, we are using the response() function passing $error->toArray() as the first parameter, and Response::HTTP_BAD_REQUEST as the second one, which is the constant of the HTTP status 400.

Final implementation

As a final result, we have a show action that can be implemented with one single line, without the need for checking for a null object nor try/catch blocks to handle the exception getting thrown.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class UsersController extends Controller
{
    public function shows(Request $request): Response
    {
        return response(User::findOrFail($request->id));
    }
}
Enter fullscreen mode Exit fullscreen mode

Now our findOrFail exception automation is done.

Happy coding!

Top comments (3)

Collapse
 
kazemmdev profile image
Kazem • Edited

Thanks, Such a good tips.
I want to point out what about adding a custom exception that only handles render function for our exception response like:

class ApiException extends Exception
{
    public function render(): JsonResponse
    {
        return response()->json($this->getMessage(), $this->getCode());
    }
}
Enter fullscreen mode Exit fullscreen mode

and then extend our exceptions based on that

Collapse
 
jackmiras profile image
Jack Miras

Hello @kazemmdev,

I'm glad you've found this article interesting and useful, the idea here is to explain how developers can take advantage of Laravel's exception handler to automate the handling of Laravel's built-in exceptions, such as the ModelNotFoundException thrown by the findOrFail() helper.

In case you want to read my thoughts on the usage and structuring of custom Exceptions that can be automatically handled by Laravel, you can take a look at the second article of this series.

There, I talk about creating a base exception that implements the render() method alongside a few other things I judged to be necessary to create a well structure error modeling for automated exception handling.

Fell free to add your thoughts in there, it would be a pleasure to have more insights on the article!

Collapse
 
kazemmdev profile image
Kazem

Hey, @jackmiras!
Thanks a bunch for getting back to me. Yeah, you're totally right, there are various ways to get things done. Personally, I always try to use as little code as possible.