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

Context Exception Laravel Package

fab2s/context-exception

A lightweight context-aware exception package for PHP and Laravel that lets exceptions carry structured contextual data, making errors easier to log, debug, and present with meaningful details across your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and read two files, not the whole package.

composer require fab2s/context-exception

Then open, in this order:

  1. src/ContextExceptionInterface.php — the contract (getContext(), setContext(), addContext() in some shape).
  2. src/ContextExceptionTrait.php — the actual behavior. This is the file that matters, because it is what you will reuse in your own exception classes. The bundled ContextException class is just the trait applied to \Exception.

The entire value proposition is one idea: an exception that carries a structured array alongside its message, so you stop encoding data into string messages.

First use case — kill your string-concatenated messages. Find a place where you currently do this:

throw new \RuntimeException("Payment failed for order {$order->id} via {$gateway}");

Replace it with:

throw new ContextException('Payment failed', [
    'order_id'  => $order->id,
    'gateway'   => $gateway,
    'amount'    => $order->total,
    'attempt'   => $attempt,
]);

You gain nothing visible yet — the win shows up in the second consumer: your log handler, your API error renderer, and your test assertions can all read getContext() instead of regex-ing a message string. That is the whole reason to adopt it, so pick your first use case where the context is actually consumed downstream (a report handler or a JSON error response), not just where it is thrown.

Verify the constructor signature in the source before writing factories. Small packages like this vary on argument order (context as second arg vs. fourth after $code/$previous). If it is ambiguous, skip direct instantiation entirely and use the trait on your own base exception — that path is stable regardless.

Implementation Patterns

Pattern 1: One project base exception, then never touch the package again

The scalable move is not to throw ContextException everywhere (you lose type-based catch and per-domain rendering). Instead, apply the trait once:

namespace App\Exceptions;

use Fab2s\ContextException\ContextExceptionTrait;

class DomainException extends \Exception
{
    use ContextExceptionTrait;
}

Then every domain exception in the app gets context for free:

namespace App\Exceptions;

class InventoryShortage extends DomainException
{
    public static function forSku(string $sku, int $requested, int $available): self
    {
        return (new self('Insufficient inventory'))
            ->setContext(compact('sku', 'requested', 'available'));
    }
}

Now catch (DomainException $e) still works, instanceof checks stay meaningful, and context is an implementation detail of the base class. If the package ever disappears, you reimplement three methods on one class.

Pattern 2: Named constructors over inline arrays

The single biggest maintainability factor with context exceptions is whether the context shape is defined in one place. Named constructors (as above) do this. Inline new InventoryShortage('...', [...]) scattered across five services does not — you end up with sku in one place, product_sku in another, and an alerting rule that matches neither.

If you want a hybrid, add a contextSchema()-style guard in the base class that asserts required keys in non-production, or just add typed properties to the exception and build the array in the constructor.

Pattern 3: Enrich, don't replace, at layer boundaries

Context shines when each layer adds what it knows:

// repository
throw OutboundHttpFailed::forEndpoint($url, $status);   // url, status

// service — catch, add domain ids, rethrow
try {
    $this->client->send($payload);
} catch (OutboundHttpFailed $e) {
    throw $e->addContext(['order_id' => $order->id, 'attempt' => $attempt]);
}

// controller / job — add correlation
catch (OutboundHttpFailed $e) {
    throw $e->addContext(['request_id' => $requestId]);
}

This is where addContext() (mutating, returns $this) vs. a withContext()-style immutable copy matters — check which the package gives you. Mutation-on-rethrow is fine and simpler; immutable copies are safer if the same exception object is caught twice (rare, but it happens in retry loops).

Always pass $previous when you wrap a different exception type. Losing the original trace is the most common self-inflicted wound in exception layering.

Pattern 4: Report hook as the single consumer

In bootstrap/app.php (Laravel 11+) or App\Exceptions\Handler:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (DomainException $e) {
        if ($context = $e->getContext()) {
            Log::error($e->getMessage(), $context + [
                'exception' => $e::class,
                'code'      => $e->getCode(),
            ]);
        }
    });
})

One place converts context into structured log fields, which is what makes the data queryable rather than decorative. Return false from the report callback if you've fully handled logging and don't want the default handler to log a duplicate record.

Pattern 5: Context in HTTP error responses

For a JSON API, map context into the payload — but through an explicit allowlist, per exception type, not getContext() verbatim:

class InventoryShortage extends DomainException
{
    public function render($request)
    {
        return response()->json([
            'error'    => 'inventory_shortage',
            'message'  => $this->getMessage(),
            'details'  => Arr::only($this->getContext(), ['sku', 'requested', 'available']),
        ], 409);
    }
}

Implementing render() on the exception is the neatest Laravel integration: controllers stay thin, and the exception owns its own HTTP representation.

Pattern 6: Assertions in tests

This is the highest-ROI daily use. Instead of expectExceptionMessageMatches() gymnastics:

try {
    $this->postJson('/api/orders', $payload);
    $this->fail('Expected InventoryShortage');
} catch (InventoryShortage $e) {
    $this->assertSame('SKU-1', $e->getContext()['sku']);
}

Or with $this->expectException() plus a small custom assertion helper on your base TestCase. Tests that assert on context stay green when you reword the human-readable message — a real reduction in test churn.

Gotchas and Tips

Laravel may already have this. Recent Laravel versions ship Exception::context() for structured logging. If you're on a version that has it, adopting a second, parallel convention means two ways to express the same thing and neither is reliably populated. Decide deliberately: use the package if you need context on domain exceptions that you also catch and inspect programmatically (context as data), and use the framework feature if you only need it for logs. Don't ship both.

Context is not logged or rendered for free. Nothing in this package knows about your logger. If you don't add a report hook (Pattern 4) or a render() (Pattern 5), the context is inert data that only shows up in var_dump. Many teams adopt the package, throw context-rich exceptions, and then wonder why logs look identical. Wire the consumer first, then start throwing.

Don't put non-serializable values in context. Queue failures serialize the exception into failed_jobs. Closures, PDO handles, Eloquent models with live connections, and streams will break or bloat that serialization. Store identifiers ($order->id, $order->getKey()), not $order. The same rule applies if you ever cache/persist exception context.

Everything in context is a potential secret leak. Passwords, tokens, full request bodies, card PANs, and PII all find their way into a catch block's convenient $request->all(). Build the scrub list before the first production incident, not after: either filter at the throw site (preferred — the throwing code knows what's sensitive), or add a single SanitizesContext trait method applied in the report hook. Note that context may end up in logs, in failed_jobs (DB), and in HTTP responses — three different exposure surfaces with different rules.

You now have duplication pressure with getMessage(). If the message says "Payment failed for order 42 via stripe" and the context has order_id/gateway, you've doubled the maintenance and the log line. Move toward short, stable, human-facing messages ("Payment failed") with all variable data in context. Watch out for anything downstream that parses the message (alerting rules, monitoring string matches) — those break silently when you shorten messages.

Exception messages get truncated; context usually doesn't. Another argument for putting bulk data in context. But check your log sink's field limits — a context array with a 200KB HTML error body from an upstream API will get dropped or truncate the whole record.

Trait method collisions. getContext() / setContext() are generic names. If a base exception class in your hierarchy already defines them (or a parent uses a trait with the same methods), PHP will fatal on conflict. Resolve explicitly with insteadof/as, or rename via the trait aliasing. Check this the moment you introduce the trait below an existing exception base.

Verify addContext() semantics before relying on it. Mutating (returns $this) versus immutable (returns a clone) changes your code shape at every rethrow site. Getting this wrong gives you throw $e->addContext(...) returning null and a confusing "exception must be Throwable" error. One var_dump-or-read-the-source check saves an hour.

Adoption risk is low but real. Three stars, zero dependents, one maintainer. The mitigation is trivial: the surface is a trait with a handful of array methods. Copy it into app/Exceptions/Concerns/HasContext.php if you want zero dependency, or accept composer require and know that a fork or inline copy is a 15-minute job. Either way, do not let ContextException (the concrete class) leak into your public API or domain layer — always extend your own base exception. That single rule makes the dependency swappable.

Extension point to actually build: a contextKeys() or redactedKeys() static on your base exception, consumed by the report hook and the renderer. This gives you one place to declare, per exception type, what is logged, what is rendered, and what is never emitted — which is the thing this package doesn't give you and you will need within a month.

When not to use it. For a one-off throw new \RuntimeException('config missing') in a bootstrap path, adding context is ceremony with no consumer. Reserve structured context for exceptions that cross a layer boundary, get reported, or get asserted on in tests.

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