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

Exceptions Laravel Package

graham-campbell/exceptions

Graham Campbell's Exceptions package gives Laravel apps a cleaner, more powerful exception handling setup, with custom handlers, reporting, throttling, and rendering so errors are easier to manage and debug.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install and publish config first:
    composer require graham-campbell/exceptions:^17.2
    php artisan vendor:publish --provider="GrahamCampbell\Exceptions\ExceptionsServiceProvider"
    
    Require ^17.2 if you're on Laravel 11 / PHP 8.4 — that's the release that officially adds those constraints. Anything lower will refuse to install or fight you on platform requirements.
  • Read config/exceptions.php top to bottom before writing any code. The array keys — filters, transformers, handlers, displayers, levels — are the mental model for everything the package does. You're configuring an ordered pipeline, not just a status-code map. If you understand that pipeline, most "why is my response shaped like this?" questions answer themselves.
  • Understand what you're swapping. This package binds a replacement for Illuminate\Contracts\Debug\ExceptionHandler. Laravel's default rendering/reporting no longer runs. Before installing, confirm which handler is bound (app(\Illuminate\Contracts\Debug\ExceptionHandler::class)) so you have a clear before/after. Half of all confusion on this package comes from people assuming they're adding a listener instead of replacing the whole pipeline.
  • Wire it up:
    • Laravel 11: bind inside bootstrap/app.php in your ->withExceptions() block — the withExceptions closure remains the framework-level configuration surface, so you'll be adding to it rather than fighting it.
    • Laravel ≤10: extend the package handler from app/Exceptions/Handler.php and swap the parent class.
  • First smoke test, before any customization: throw a 404 and a 500 from a route, then hit each from a browser (HTML) and from curl -H "Accept: application/json" (JSON). Confirm four distinct response shapes, and confirm they're coming from the package (add a marker key in config temporarily if you need to verify). This is your baseline — anything that diverges later is a config regression.
  • Most common first real use case: consistent API error envelopes. Instead of a stack-dependent {"message": "..."} blob, you define stable error codes and payload shapes per exception class and stop scattering try/catch + abort() + response()->json() blocks through controllers. That's the day-one ROI and the reason most teams install this.

Implementation Patterns

  • Exception-class-to-status mapping in config. The handlers array is ordered — the first match wins. Add entries from most specific to least specific (specific ValidationException, then ModelNotFoundException, then a catch-all Throwable fallback). Never rely on insertion order for anything you didn't explicitly reason about.
  • Custom handlers per exception type. Each entry is a class implementing HandlerInterface that produces an ExceptionInfo object from the exception. Keep these pure — they should only inspect the exception and return metadata. If you need the current user, a request ID, or a correlation ID, resolve it at throw time (via a middleware-bound context or a request-bound singleton) and read it back from the handler, not by hitting the DB from the handler.
  • Transformers normalize the exception object. If you have custom exception classes across your domain, transformers let you turn PaymentDeclinedException, InventoryException, etc. into a uniform shape so the handlers and displayers only ever see one type. This is where you add fields like code, type, retryable — the bits that your API contract cares about.
  • Displayers control the envelope. Register a JSON displayer for API consumers and an HTML displayer for browsers. This is where you lock down your error payload shape — status, code, human-readable message, trace_id if you have one. Keep the JSON shape stable and version it in docs; clients will hardcode against it.
  • Use levels for per-exception log severity. A ValidationException should be info or debug; a ModelNotFoundException is notice; a QueryException is error/critical. Getting this right is one of the biggest signal-to-noise wins — most teams end up here because they're drowning in 4xx noise in their logs.
  • Filters keep the pipeline from over-processing. Filters run first and can short-circuit. Use them to skip exceptions you want the framework to handle natively (e.g. HttpResponseException, or anything already shaped by a third-party package that owns its own rendering).
  • Reporting integration lives in one place. If you're wiring Sentry/Bugsnag/OTel, route the report() / reporting path through a single adapter class rather than letting each handler reach into a vendor SDK. The levels config plus one reporting entry point is the cleanest layering.
  • Ship behind a config flag with a fallback. Put the handler-swap behind an env var and keep a render() override on the native handler as the rollback. You want to be able to flip back in one deploy, not one sprint.

Gotchas and Tips

  • Handler ordering is silently significant. First match wins in the handlers array. If your catch-all Throwable handler is above a specific ValidationException handler, every validation error will render as a 500. This bite is frequent and non-obvious — keep the catch-all at the bottom and comment it.
  • php artisan config:cache will expose any closure-in-config. Config values must be serializable. If you're tempted to inline a closure for handler selection, don't — use a class reference instead. Run config:cache then hit your smoke-test routes as a required CI step; this catches cached-config divergence that only shows up in staging.
  • Double reporting is very easy to introduce. If Laravel's report() is still firing somewhere (a Handler override that calls both, or a listener that reports alongside the package's reporting), you'll get two Sentry events per error and a doubled error-rate dashboard. Verify with a single throw + a vendor sandbox before shipping.
  • A handler that throws will produce a blank 500 — not a graceful degrade. Add a defensive try/catch around custom handlers, or at minimum ensure your renderers are pure mappings with no I/O (no DB, no cache, no outbound HTTP). Error paths are hit precisely when the system is degraded, so make them the one code path that can't fail.
  • Production detail leakage is the classic misconfiguration. Confirm that debug is not just derived from APP_DEBUG but that the displayer explicitly strips stack traces and internal fields when APP_ENV=production. Test by flipping APP_DEBUG=false locally and hitting a 500 — if you see internal hostnames, you have a bug.
  • On Laravel 11, the withExceptions() closure and the package's config are two surfaces that both touch the same behavior. The package wins for rendering/reporting once bound, but withExceptions() still owns shouldRenderJsonWhen, dontReport, and the renderable() closure. Document which surface owns what in a comment in bootstrap/app.php — reviewers and future-you need this. If you find both configured for the same exception, the package handler is authoritative; remove the framework override to avoid confusion.
  • Queue jobs and failed_jobs go through the same reporter. If you're mapping exceptions to log levels, consider whether a job exception should have the same level as a request exception — often not. You may need a filter that inspects app()->runningInConsole() and adjusts.
  • Octane / long-lived workers: handler instances get reused across requests. If any custom handler caches per-request context (user, correlation ID) on the instance, you'll leak between requests. Prefer pure handlers, or resolve context per-call from a scoped service.
  • Keep the set of custom handler classes small and boring. Every class is a thing you have to read, test, and eventually re-implement when you build an exit plan. If a handler is more than ~30 lines, it's probably doing rendering work that belongs in a displayer or enrichment that belongs at throw time.
  • Version-pin and lock. With ~0 dependents and a low adoption score, this package owns a privileged slot in your request lifecycle with limited external validation. Commit composer.lock, keep the customization surface thin, and document a one-sprint exit path (revert to Laravel's native handler + a small render() override) before you go past v1.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource