Laravel Exceptions Enhanced
Published: 2024-03-25 12:47:04

Hi! Today I want to share the approach that I use when I need to handle custom Exception in my Laravel projects.

### Why is it important to create and use custom Exceptions?

<div data-node-type="callout">
<div data-node-type="callout-emoji">🙃</div>
<div data-node-type="callout-text">You may skip this section if you are just interested in the code.</div>
</div>

The Exceptions that PHP provides out of the box offer little help to understand the issue from the logs.

They accept just two params: `message` and `code`. Usually the message contains context for us developers, and therefore cannot be given to the user. The code is not used by Laravel so will render the very generic 500 error page.

In addition, we do not have any control of what will be given to the user. The `json` response returns just a basic `message` property an the blade file shows a default message based on the `statusCode`.

### Custom Exceptions

We first create an abstract class called `BaseException`; this class won't have any abstract functions, some of you may argue that it's unnecessary and it can be just a normal class, but making it abstract we make it clear that it should not be used by itself.

```php
namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

abstract class BaseException extends Exception
{
    public const STATUS_CODE = Response::HTTP_INTERNAL_SERVER_ERROR;

    public function __construct()
    {
        parent::__construct($this->getTranslatedMessage(), static::STATUS_CODE);
    }

    public function render(Request $request): Response|false
    {
        if ($request->expectsJson()) {
            return new Response(
                [
                    'error' => [
                        'code' => static::STATUS_CODE,
                        'message' => $this->getTranslatedMessage(),
                    ],
                ],
                static::STATUS_CODE,
            );
        }

        abort(static::STATUS_CODE, $this->getTranslatedMessage());
    }

    protected function getTranslatedMessage(): string
    {
        return trans('exceptions.'.static::class);
    }
}
```

Let's analyze the class:

* The `getTranslatedMessage` method will return the translated message that we want to give to our users.
    
* The `render` method will make sure that the JSON response structure is returned *always* the same way. Obviously you can personalize it based on your needs.
    
* The constant `STATUS_CODE` can be overridden when extending it or use the default 500 error code.
    

<div data-node-type="callout">
<div data-node-type="callout-emoji">🤔</div>
<div data-node-type="callout-text">Note that we're using <code>static</code> instead of <code>self</code> - this way we're making sure we're using the value from the extending class instead of using the abstract default values.</div>
</div>

We can now create custom Exceptions like so

```php
class DatabaseOffline extends BaseException
{
    public const STATUS_CODE = Response::HTTP_SERVICE_UNAVAILABLE;
}
```

We should name exceptions as descriptive as we can. This will immediately help us understand the problem when reading it in the logs. Sometimes, such as in this case, we can skip looking at the code entirely.

### The translation files

The next step is setting up our language files. Create the file `exceptions.php` for each language that you support in your application.

```php
// lang/en/exceptions.php
use App\Exceptions\DatabaseOffline;

return [
    DatabaseOffline::class => 'Unfortunately the system seems to be offline. Try again later.',
];

// lang/it/exceptions.php
use App\Exceptions\DatabaseOffline;

return [
    DatabaseOffline::class => "Purtroppo il sistema sembra essere offline. Riprova più tardi.",
];
```

Using the exception fully qualified name class as the array key will make sure that if the class name is changed, your IDE will rename it here too; using a custom key like `database-offline` will then require us to manually change it.

### Personalize Default Error Pages

Laravel offers blade pages render exception; this out of the box feature is limited and requires a bit of customisation from us.

To begin, let's publish them so we can modify them

```basic
php artisan vendor:publish --tag=laravel-errors
```

This will create the blade files in `resources/views/errors/*.blade.php`

I leave you to modify all of them, I'll show you an example:

```php
@extends('errors::minimal')

@section('title', __('Server Error'))
@section('code', $exception->getStatusCode())
@section('message', $exception->getMessage() ?: __('Server Error'))
```

Let's throw the exception

```php
throw new DatabaseOffline();
```

If we access the page from the browser we will see:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711184436717/a5d7bff0-b3b3-44d4-9017-330f5df04ff4.png align="center")

If we, instead, send the request from a javascript application, we will see:

```json
{
    "error": {
        "code": 503,
        "message": "Unfortunately the system seems to be offline. Try again later."
    }
}
```

### Test

I like to have my code tested to make sure it behaves correctly at any change I make. I leave the below Pest test in case you find it useful.

```php
// tests/Pest.php
function supportedLocales(): array
{
    return [
        'en',
        'it',
        // add future supported locales here
    ];
}

// tests/Unit/Exceptions/TranslatableExceptions.php
use App\Exceptions\DatabaseOffline;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;

it('returns a translated message', function (string $locale, string $exception) {
    Config::set('app.locale', $locale);

    $exceptions = include_once lang_path(sprintf('%s/exceptions.php', $locale));

    $this->expectExceptionCode($exception::STATUS_CODE);
    $this->expectExceptionMessage($exceptions[$exception]);

    throw new $exception;
})->with(Arr::crossJoin(
    supportedLocales(),
    [
        DatabaseOffline::class,
        // Add future exceptions here
    ],
));
```

If you have any questions, please leave a comment below.