Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Notifiable Exception Laravel Package

cerbero/notifiable-exception

View on GitHub
Deep Wiki
Context7

Getting Started

cerbero/notifiable-exception is a small, zero-config package: it lets a specific exception class carry its own notification logic, so that when Laravel's exception handler reports it, the right people get pinged instead of the event disappearing into storage/logs/laravel.log.

composer require cerbero/notifiable-exception

There is nothing to publish — no config file, no migration, no service provider to register (it's auto-discovered). Check compatibility before you commit:

composer show cerbero/notifiable-exception   # confirm PHP/Laravel constraints

Where to look first. The package is a handful of files. Read vendor/cerbero/notifiable-exception/src/ in five minutes — the contract is the whole API, and the README's example is the canonical shape. Every notifiable exception answers two questions: who should be notified and what notification should be sent. Names may vary slightly between versions, so confirm the exact methods against the interface before writing ten exception classes.

First use case. A business-level failure that a human must act on, not a stack trace you'll grep later:

namespace App\Exceptions;

use App\Notifications\PaymentFailedNotification;
use Cerbero\NotifiableException\NotifiableException;
use Illuminate\Notifications\Notification;
use RuntimeException;

class PaymentFailedException extends RuntimeException implements NotifiableException
{
    public function notifiables(): array
    {
        return User::where('role', 'billing')->get();
    }

    public function toNotification(): Notification
    {
        return new PaymentFailedNotification($this->getMessage());
    }
}

Throw it as usual — the notification fires when the exception is actually reported, i.e. when it reaches the handler uncaught:

throw new PaymentFailedException("Gateway declined order {$order->id}");

Because it rides on top of Laravel's notification system, you immediately get mail, Slack, SMS, database channels, ShouldQueue, and Notification::fake() in tests for free.

Implementation Patterns

Opt-in per exception class. This is the package's real value: nothing is notifiable unless you say so. Reserve it for exceptions that represent a business condition (payment declined, webhook signature invalid, import failed, third-party quota exhausted). Let genuine bugs go to your error tracker.

Keep a base class in your app. Rather than repeating the contract on every exception, define one:

abstract class NotifiableException extends RuntimeException implements NotifiableException
{
    public function notifiables(): array
    {
        return User::whereIn('role', ['admin', 'oncall'])->get();
    }
}

Subclasses then only override toNotification(). If a subclass needs different recipients, it overrides notifiables() too.

On-demand routing for ops channels. Slack/ops channels aren't Notifiable models. Return an empty array (or omit the model lookup) and route explicitly:

public function notifiables(): array
{
    return [Notification::route('slack', config('services.slack.alerts_webhook'))];
}

Capture context at throw time, not at notification time. By the time report() runs, auth(), the request, and the session may be in an odd state — especially in queue workers. Pass scalars into the exception constructor:

throw new ImportFailedException($import->id, $import->user_id, $rowCount);

and let the notification read those properties. Never re-query the database from inside a notification that may run on a queue after the row was deleted.

Deduplicate before you fan out. A failing cron job can throw the same exception every minute:

public function notifiables(): array
{
    $key = 'notify:import-failed:'.$this->importId;

    return Cache::add($key, true, now()->addMinutes(15))
        ? User::where('role', 'oncall')->get()
        : [];
}

Returning an empty array means "report it, but stay quiet."

Escalate on repetition. First occurrence Slack, third occurrence email the lead — the exception object is a fine place to encode that threshold logic, since it's constructed fresh on each throw.

Queue the notification, not the exception. Implement ShouldQueue on the notification and pass scalars/IDs to it; the notification's constructor should not hold the Throwable (see Gotchas). If you need the stack trace in the payload, store the formatted string eagerly:

new ImportFailedNotification($this->importId, $this->getTraceAsString())

Test it like any other notification. The package adds no testing surface of its own:

Notification::fake();

try {
    $this->post('/imports');
} catch (ImportFailedException $e) {
    // or use $this->withoutExceptionHandling()
}

Notification::assertSentTo($admin, ImportFailedNotification::class);

Note that testing through the HTTP layer requires the exception handler to actually run — withoutExceptionHandling() will bypass the reporting path entirely.

Gotchas and Tips

Only reported exceptions notify. If something catches your exception and swallows it, no notification is sent — by design. Conversely, in queue workers, failed() jobs and retries can report the same exception multiple times. Combine the package with the dedupe cache above, or you'll learn this from your Slack bill.

Returning false from the reporting method silences logging. Laravel's handler honours a false return from an exception's report hook to skip default reporting. If you override the hook to add your own logic, don't accidentally return false — you'll lose the log entry while keeping the Slack ping.

Serialization is the sharpest edge. A queued notification is serialized into the job payload. An Exception instance carries its trace, file contents, and potentially closures, PDO handles, or the whole request via previous. Queued notifications built around new MyNotification($this) frequently fail with Serialization of 'Closure' is not allowed or bloat to hundreds of KB. Keep exceptions out of queued notifications.

Beware notification-induced recursion. If a channel throws (bad Slack webhook, malformed mail config), that exception is reported too — and if it's notifiable, you get a loop. Wrap the send in a try/catch and log locally, or ensure your notifiable exceptions are leaf nodes that never touch notification infrastructure.

Don't confuse this with error tracking. Sentry/Bugsnag/Flares already handle "something broke." This package is for "something happened that a person must know about now." If you're wiring QueryException into it, you've built a worse Sentry — reach for the tracker instead.

Environment guards are on you. Nothing stops notifications firing in local or CI. Either add if (app()->environment('local', 'testing')) return []; to your base class, or Notification::fake() in your test suite. Forgetting this produces a memorable afternoon of Slack spam from a test suite.

Respect $dontReport. HttpException, ModelNotFoundException, and auth exceptions are already excluded by Laravel's internal list, and anything you add to App\Exceptions\Handler::$dontReport is reported-never — so the package silently does nothing for those. If a notification "isn't firing," check that list before you debug the package.

The package is small and lightly maintained (76 stars, no dependents). It's ~a few hundred lines that hook into an existing Laravel convention, so the blast radius is low — but pin the version in composer.json and read the source rather than trusting the docs. If it ever breaks against a new Laravel major, replicating it yourself is a one-afternoon job: implement the reporting hook on a base exception and call Notification::send().

Extension point: the package deliberately doesn't own delivery — every queue, channel, retry, and rate-limit concern is Laravel's. Custom channels, ShouldQueue, via() filtering by recipient preference, and notification events all work normally, which is the best reason to use this over a bespoke Mail::raw() in a catch block.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel