You should not interrupt Laravel's exception handling
Tags: web, php, laravel
Published: 2022-10-29 13:34:00

---
title: You should not interrupt Laravel's exception handling
published: true
description: Discover a better way to handle errors to not miss any of them.
tags: web, php, laravel
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1yxtsanjarptwl6klhmp.png
# Use a ratio of 100:42 for best results.
published_at: 2022-10-29 13:34 +0000
---

Hi and welcome back for another article. I will give you my knowledge on my experience dealing with code that interrupt Laravel's default error handling, and how to mitigate issues that can occur when doing so.

- [Context](#context)
- [Configuring the error handler](#configuring-the-error-handler)
- [Why try-catch alone is an issue](#why-trycatch-alone-is-an-issue)
- [Solution #1: using `report()`](#solution-1-using-raw-report-endraw-)
- [Solution #2: using `rescue()`](#solution-2-using-raw-rescue-endraw-)
- [Conclusion](#conclusion)

## Context

Let us say your user is logged in its back-office, and can manage users. Your back-office user is allowed to create some users.

```php
namespace App\Http\Controllers;

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

class UserController
{
  public function create()
  {
    return view("user.create");
  }

  public function store(Request $request)
  {
    User::create(["name" => $request->string("name")]);

    return redirect()->route("user.index")->withSuccess("User stored.");
  }
}
```

At some point, your database went down, and back-office users came to you and reported that they saw an "Oops - something went wrong" page.

You decide to make a custom validation error just so that users stay on the same page, and you still log the error to fix it later on.

```diff
  namespace App\Http\Controllers;

  use App\Models\User;
+ use Exception;
  use Illuminate\Http\Request;
+ use Illuminate\Support\Facades\Log;

  class UserController
  {
    public function create()
    {
      return view("user.create");
    }

    public function store(Request $request)
    {
+     try {
        User::create(["name" => $request->string("name")]);
+     } catch (Exception $exception) {
+       Log::error($exception->getMessage());
+
+       return redirect()->back()->withErrors("Unable to store the user.");
+     }

      return redirect()->route("user.index")->withSuccess("User stored.");
    }
  }
```

## Configuring the error handler

Let us imagine your error are not only logged on log file (by default), but also sent on [Sentry](https://sentry.io) to help debugging easily.

By the way, here is how an error renders on Sentry.

![Example of an error catched on Sentry](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y227p78b0n51o9m45ldz.png)

Here is how you have configured the file `app/Exception/Handler.php` to do so.

```php
namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\App;
use Throwable;

class Handler extends ExceptionHandler
{
  /**
   * A list of the exception types that are not reported.>
   */
  protected $dontReport = [
    //
  ];

  protected $dontFlash = [
    'current_password',
    'password',
    'password_confirmation',
  ];

  public function register(): void
  {
    $this->reportable(function (Throwable $e) {
      /**
       * @see https://docs.sentry.io/platforms/php/guides/laravel/#install
       */
      if (!App::environment('local') && $this->shouldReport($e) && app()->bound('sentry')) {
        app('sentry')->captureException($e);
      }
    });
  }
}
```

## Why try-catch alone is an issue

Now that our error handler is modified, Laravel will **automatically** handle exception this way:

- Run through all error handlers (log, Sentry)
- Render the default error page ("Oops - something went wrong")

But since our code use try-catch, the exception is no longer thrown, plus we instruct Laravel to only send it on the file log. 

This is where things are broken: you no longer benefit from Laravel's default error handling configuration, and the next time you want to figure out why the error was not reported to Sentry, you'll have an hard time remembering every place you interrupted Laravel's error handling.

## Solution #1: using `report()`

This is the easiest, low effort solution to mitigate this issue. 

Using [`report()`](https://laravel.com/docs/9.x/helpers#method-report) is designed to help you pass the exception instance through Laravel's error handling, but without rendering any response. 

In our case it's perfect because we decided to bring our custom rendering when such error occurs, so let's implement it.

```diff
  namespace App\Http\Controllers;

  use App\Models\User;
  use Exception;
  use Illuminate\Http\Request;
- use Illuminate\Support\Facades\Log;

  class UserController
  {
    public function create()
    {
      return view("user.create");
    }

    public function store(Request $request)
    {
      try {
        User::create(["name" => $request->string("name")]);
      } catch (Exception $exception) {
-       Log::error($exception->getMessage());
+       report($exception);
 
        return redirect()->back()->withErrors("Unable to store the user.");
      }

      return redirect()->route("user.index")->withSuccess("User stored.");
    }
  }
```

Next time an error is catched, it will be sent to the report error handler of Laravel, and the validation error will be return in the create user page, which preserves logging both on the log file and on Sentry.

## Solution #2: using `rescue()`

This one is my favorite because the code becomes more readable.

Rescue will catch **any exception** that occurs, send it on the default exception handling mecanism, and allow you to return a custom value in this case.

```php
namespace App\Http\Controllers;

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

class UserController
{
  public function create()
  {
    return view("user.create");
  }

  public function store(Request $request)
  {
    $stored = rescue(function () use ($request) {
      User::create(["name" => $request->string("name")]);

      return true;
    }, false);
      
    if (!$stored)
      return redirect()->back()->withErrors("Unable to store the user.");
    }

    return redirect()->route("user.index")->withSuccess("User stored.");
  }
}
```

As you can see, in first parameter, if the callback resolved successfully, rescue will return its value (`true` in this case), and if any exception was catched, it will return the second parameter value (`false`).

For the ones that like one liners, here is a condensed version.

```php
namespace App\Http\Controllers;

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

class UserController
{
  public function create()
  {
    return view("user.create");
  }

  public function store(Request $request)
  {
    return rescue(function () use ($request) {
      User::create(["name" => $request->string("name")]);

      return redirect()->route("user.index")->withSuccess("User stored.");
    }, function () {
      return redirect()->back()->withErrors("Unable to store the user.");
    });
  }
}
```

One important note is that `rescue()` will catch **any** exception that occurs, so if you have the need to send a specific message for a given exception, `rescue()` will not be suited and you should probably opt for using the 1st solution.

```php
namespace App\Http\Controllers;

use App\Models\User;
use Exception;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;

class UserController
{
  public function create()
  {
    return view("user.create");
  }

  public function store(Request $request)
  {
    try {
      User::create(["name" => $request->string("name")]);
    } catch (QueryException $exception) {
      report($exception);

      return redirect()->back()->withErrors("Unable to store the user because the storage system did not respond.");
    } catch (Exception $exception) {
      report($exception);

      return redirect()->back()->withErrors("Unable to store the user.");
    }

    return redirect()->route("user.index")->withSuccess("User stored.");
  }
}
```

## Conclusion

I hope this post was useful for you and that it will help you write more solid code given this could potentially help you being more aware of errors, mostly if your error handling mecanism use different logging solutions.

There is probably a few scenarios where neither `report()` nor `rescue()` would answer the need, so let me know if you have some example on the comment section!

That is all I have for today, I hope you will leave with new perspectives in mind after reading this article.

Happy error handling 🐞