Poor man's exception email notifications for Laravel

Exception Handling

If you still haven't caught on to using a proper exception management for your Laravel apps like FlareApp.io, you might want to follow this poor man's guide which will email you every time an exception takes place. It's a very primitive form of getting notified, but maybe for some super budget conscious situation it will be suitable.

I'm documenting it here because it's quite a lot of boilerplate and moving parts. To be honest, I don't recommend this even for a hobby project, but here goes anyway:

For this elaborate contraption you'll need at least:

  • A config file to where the exceptions are supposed to be sent
  • The custom Handler code
  • A mailable and a blade
    • The blade needs to be independent of Laravel, because when the exception occurs you don't have access to the framework
  • Finally, you'll have to register the handler in `bootstrap/app.php`, which differs quite a bit between Laravel versions.

Config File

Save as `config/exceptions.php`:

<?php

return [    
    'recipients' => [
        'user1@example.com',
        'user2@example.com',   
    ]
];
lang-php

Handler Code

<?php

namespace App\Exceptions;

use App\Mail\ExceptionOccurredMail;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Throwable;

class Handler extends ExceptionHandler
{    
    /**
     * Register the exception handling callbacks for the application.
     */
    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            $this->sendEmail($e);
        });
    }

    /**
     * Send an email every time an exception occurs.
     */
    protected function sendEmail(Throwable $exception): void
    {
        try {
            $emailRecipients = config('exception.recipients', ['default-email@example.com']);

            Mail::to($emailRecipients)->send(new ExceptionOccurredMail($exception));
        } catch (Exception $exception) {
            // Log a critical exception to avoid an infinite loop of exceptions
            Log::critical('Critical error to avoid infinite loops: Failed to send exception email: '.$exception->getMessage());
        }
    }
}
lang-php

Mailable

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class ExceptionOccurredMail extends Mailable
{
    use SerializesModels;

    public \Throwable $exception;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(\Throwable $exception)
    {
        $this->exception = $exception;
    }

    public function build()
    {
        return $this->with([
            'exception' => $this->exception,
        ]);
    }

    /**
     * Get the message envelope.
     */
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: config('app.name') . ' Exception Occurred',
        );
    }

    /**
     * Get the message content definition.
     */
    public function content(): Content
    {
        return new Content(
            view: 'emails.exception',
        );
    }

    /**
     * Get the attachments for the message.
     */
    public function attachments(): array
    {
        return [];
    }
}
lang-php

Blade

Note the vanilla format since you won't have access to the rest of the Blade templating in the framework.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Exception Occurred</title>
</head>
<body>
<h1>A {{ config('app.name') }} exception has occurred</h1>
<p>Exception message: {{ $exception->getMessage() }}</p>
<p>File: {{ $exception->getFile() }}</p>
<p>Line: {{ $exception->getLine() }}</p>
</body>
</html>
lang-html

`bootstrap/app.php` modifications

Here are two variants of the Handler code in `bootstrap/app.php`:

Older version

<?php

...

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/

return $app;
lang-php

Newer version

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

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) {
        App\Exceptions\Handler::class;
    })->create();
lang-php

End.