tmsllc/laravel-exception-mail
Laravel package that emails exception details straight to your inbox so you can monitor production errors without a third-party service. Simple configuration integrates with Laravel's existing exception handler and alerts you the moment something breaks.
laravel-exception-mail pipes unhandled exceptions out through Laravel's mail layer, so you get an inbox notification instead of (or alongside) a log line. Version 1.0.1 is a compatibility patch that adds Laravel 11 support — no new features or breaking changes — so if you are already on 1.0.0 with Laravel 10, upgrading is a no-op; if you are on Laravel 11, 1.0.1 is the first version you can install.
composer require tmsllc/laravel-exception-mail:^1.0.1
php artisan vendor:publish --provider="Tmsllc\ExceptionMail\ExceptionMailServiceProvider"
Laravel 11 auto-discovers the service provider, so there is nothing to register in bootstrap/providers.php. Before wiring anything up:
config/exception-mail.php or similar) and read every key — recipients, enabled, environment gating, and any throttling/deduplication settings are the four things you actually care about.App\Exceptions\Handler is gone and reporting is configured through bootstrap/app.php → ->withExceptions(...). Confirm the package listens on the container's exception handler / reportable pipeline rather than expecting an app-level Handler class you no longer have.[email protected]) rather than hardcoding addresses in config, so staging and production can diverge.First use case: you have a production app with no APM and no Sentry budget. You want a mail when a 500 fires, with the exception class, message, file:line, and a trimmed stack trace. Install, set recipients, force an exception on a staging box, confirm the mail arrives — done in ten minutes.
Environment-gate it, always. You do not want a mail per exception on every developer's machine. Either toggle the package's enabled flag from APP_ENV, or rely on Laravel's own gating:
// config/exception-mail.php
'enabled' => app()->environment(['production', 'staging']),
Queue the mail. Sending synchronously from inside the exception handler blocks the response and, on a slow SMTP server, turns a 500 into a 30-second 500. Push the mailable onto the queue and make sure a queue worker is actually running — a queued alert with no worker is silently the same as no alert.
Throttle by fingerprint, not by time. The naive "at most one mail per minute" config still lets a loop in a hot endpoint send one mail per minute forever. Prefer packages that dedupe on a fingerprint (exception class + file + line) or add your own RateLimiter key in a wrapper.
Prune the noise list. Validation, auth, and 404s are not incidents:
// bootstrap/app.php (Laravel 11)
->withExceptions(function (Exceptions $exceptions) {
$exceptions->dontReport([
\Illuminate\Validation\ValidationException::class,
\Illuminate\Auth\AuthenticationException::class,
\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
]);
})
dontReport runs before the package's hook, so this is the cheapest place to suppress — no package config needed.
Wrap the send in a guard. If the mail transport throws inside the exception handler you get a nested exception and often a blank response. If the package does not already do this, decorate it:
try {
Mail::to(config('exception-mail.to'))->queue(new ExceptionOccurred($e));
} catch (\Throwable $mailFailure) {
Log::error('exception-mail: failed to send alert', ['original' => $e, 'mail' => $mailFailure]);
}
Test it with Mail::fake(). In a feature test, hit an endpoint that throws and assert the mailable was queued — that is the only way to know the package is wired into your handler chain after a Laravel upgrade reshuffles bootstrap/app.php.
Maximum function nesting level in the logs while testing, that is the failure mode — guard the send.MAIL_MAILER=log in production is the classic silent-alert outage. Assert on MAIL_MAILER (and a working From address — SES/SendGrid reject unverified senders) at deploy time, or you will discover it during the incident.1.0.0 for Laravel ≤10, ^1.0.1 for Laravel 11. Check the composer.json illuminate/* constraints before upgrading a Laravel 10 app — a patch bump that widens a constraint usually still allows 10, but verify rather than assume.shouldReport callback. These are the seams to use before forking.How can I help you explore Laravel packages today?