Handling Laravel's 'ModelNotFoundException' globally

Nav • May 24, 2020

laravel

Update: See bottom of article to see how to handle while declairing the route.

Laravel offers a pretty elegant way of handling exceptions. Let's say you wanted to retrieve a record from a database, and for some reason it's not found. Maybe the user entered the wrong id in the search field, etc.. We need a good way to handle that exception, and display errors.

One way to handle it...

You could do something like this in your controller. We're trying to find a record in inventory model.

use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;

public function getRecord($id)
{
    try {
        $inventory = Inventory::findOrFail($id);
        // optionally
        // $inventory = Inventory::firstOrFail($id);
        return view('inventory.show', compact('inventory'));
    } catch (ModelNotFoundException $e) {
        if ($e instanceof ModelNotFoundException) {
            return back()->withError('Record not found');
        }
    }
}

The user enters an inventory id in a search field, and if it does not exist in the database the user will be returned back to the previous page with errors, much like validation errors. You could display this error in a alert block.

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

So now you have a nicer way to handle ModelNotFound exception errors. All this works, but adding that logic in your controllers over and over can get tedious. So...

Another way to handle it...

We could add this logic in app/Exceptions/Handler.php. In which case that application will be available throughout your application. Open app/Exceptions/Handler.php and update the render method.

use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;

public function render($request, Throwable $exception)
{
    if ($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
        return back()->withErrors(
            ["message" => 'Record not found.'],
        );
    }

    return parent::render($request, $exception);
}

Now in your controller, you can do something like this:

public function getRecord($id)
{
    $inventory = Inventory::findOrFail($id);
    // optionally
    // $inventory = Inventory::firstOrFail($id);
    return view('inventory.show', compact('inventory'));
}

If the record is not found, it will return back to the previous page, rendering the error in the alert block. I think this is a much cleaner way to handle this.

Update

There is another way to handle this while declairing the route. You can redirect to a model not found view. See example below.

Route::get('/blog/{slug:slug}', [BlogController::class, 'show'])
        ->name('blog.show')
        ->missing(function (Request $request) {
            return Redirect::route('blog.article-not-found');
        });