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

Error Handler Laravel Package

slick/error-handler

A Laravel error handler that centralizes exception handling, rendering, and reporting. Configurable handlers convert PHP exceptions into consistent responses, with hooks for logging, custom views, and per-type handling to keep error output predictable across your application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and locate the code

composer require slick/error-handler:^0.5

Note the version: the package is on a 0.x line, latest v0.5.0. Under semver that means any minor bump may break you, so pin tighter than the caret for production ("slick/error-handler": "0.5.*") and read the release notes before every composer update.

The package is small, so read it before trusting it: start with composer.json (check the autoload PSR-4 prefix, typically Slick\ErrorHandler\), then src/. You are looking for three things:

  1. The handler/registrar class — the thing that hooks into PHP's error pipeline.
  2. The formatter contract — the interface you implement to control output.
  3. The bundled formatters (HTML/plain-text/CLI/JSON) — the ones you will actually reuse.

v0.5.0 is a maintenance release whose only listed change is a fix to source code detection — i.e. the logic that locates and slices the source lines around the failing file/line for display. That tells you source-context rendering is a real, actively-used feature of the formatters, so look at how it resolves paths before you rely on the snippets.

Adoption is still effectively nil (0 stars, 0 dependents in the data we have) and the 0.x versioning means the API is not frozen. Treat the source as the documentation.

Minimal wiring

The handler needs to be registered as early as possible — before your framework boots, so that bootstrap failures are also caught:

// public/index.php or bootstrap/app.php, before the kernel is created
$handler = new \Slick\ErrorHandler\ErrorHandler(/* formatter, debug flag */);
$handler->register();

First use case

Do not try to replace Laravel's exception handler on day one. The highest-value first use is the place Laravel's handler cannot reach:

  • artisan commands and queue workers that die during container/bootstrap construction
  • standalone PHP services, cron scripts, and microservices that are not full Laravel apps
  • a php -S or CLI entrypoint where you want a formatted, readable trace instead of a blank 500

Get that working first, confirm a thrown RuntimeException renders correctly with the source snippet showing the right lines, then decide whether it earns a place inside the HTTP stack.

Implementation Patterns

Per-SAPI formatting

The core value proposition is "display in a given format," so make the formatter a function of the environment rather than a hardcoded choice:

$formatter = PHP_SAPI === 'cli'
    ? new PlainTextFormatter()
    : new HtmlFormatter(debug: $config->get('app.debug'));

For JSON APIs, a JSON formatter that emits a stable envelope (error, message, trace) lets your frontend parse failures from non-Laravel endpoints with the same code it uses for Laravel's {message, exception, ...} payload. If the bundled formatters also emit source context, decide deliberately whether that belongs in the JSON payload — usually it does not.

Middleware integration (PSR-15)

If you run Slim/Mezzio/Laminas, the natural fit is a thin middleware that delegates to the handler only for the terminal, uncaught case — the framework's own error middleware stays in charge of normal 4xx/5xx:

final class FallbackErrorMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        try {
            return $handler->handle($request);
        } catch (Throwable $e) {
            // handler formats, you build the response
            return new TextResponse($this->errorHandler->render($e), 500);
        }
    }
}

The split matters: let the package own formatting, keep your app owning response construction and logging.

Adopting it inside Laravel without fighting Laravel

Laravel's Illuminate\Foundation\Exceptions\Handler already does report/render. There are two clean integration points:

  • Reporting: call the package's formatter from Handler::report() for a compact, human-readable string in your log channel — useful when you want the same trace shape in logs as in the CLI. Be careful with source snippets here: multi-line source in a log line is noise; a structured formatter you own is better.
  • Non-HTTP fallback: register the package in bootstrap/app.php before Application::configure() so parse/bootstrap failures during php artisan get formatted output; Laravel takes over once the container exists.

Do not register it globally alongside Laravel's handler for web requests — both will try to emit output and you'll get doubled traces or "headers already sent."

Custom formatter as the extension point

The formatter contract is where the package pays for itself. Implement it to emit whatever your ops stack consumes — a single-line structured log, a Slack block, a Sentry breadcrumb:

final class StructuredFormatter implements FormatterInterface
{
    public function format(Throwable $e): string
    {
        return json_encode([
            'type'    => $e::class,
            'message' => $e->getMessage(),
            'file'    => $e->getFile() . ':' . $e->getLine(),
            'trace'   => array_map(
                fn($f) => ($f['class'] ?? '') . ($f['type'] ?? '') . ($f['function'] ?? ''),
                $e->getTrace()
            ),
            'previous' => $e->getPrevious()?->getMessage(),
        ], JSON_THROW_ON_ERROR);
    }
}

Testing

Skip the registration path in tests. Instantiate the formatter and assert on its string output for a known exception — fast, deterministic, no global state:

$out = (new PlainTextFormatter())->format(new RuntimeException('boom'));
$this->assertStringContainsString('RuntimeException: boom', $out);

Test the debug/non-debug split explicitly: assert that file paths and traces are absent when debug is off. That is the one behaviour that will bite you in production.

Because v0.5.0 changed source-code detection, add a test that pins the snippet behaviour you depend on: throw from a known fixture file, assert the quoted source line appears (debug on) and is absent (debug off). If a future 0.x release reworks detection again, that test tells you immediately instead of in production logs.

Gotchas and Tips

Registration order is the whole game. If Laravel, Symfony, or another handler registers after you, it wins. If you register after, you may double-handle. Verify with a deliberate throw in each entrypoint and check you see exactly one trace.

Fatal errors are not exceptions. Uncaught E_ERROR, parse errors, and memory exhaustion never reach a Throwable handler. You need register_shutdown_function + error_get_last() alongside the package's registration. Confirm whether the handler does this; if not, add it yourself or your most painful failures stay invisible.

error_reporting and @ suppression. A handler that ignores the current error_reporting() level will resurrect warnings you deliberately silenced, and set_error_handler callbacks that return false fall through to PHP's native handler. Check the return value semantics in src/ before assuming.

Source detection is best-effort — assume it will fail for you somewhere. v0.5.0 is a fix in exactly this area, which means it has been brittle. Expect misses or garbage snippets for:

  • code that doesn't exist as a readable file at runtime: eval(), create_function-style closures, code inside a .phar, stream wrappers (s3://, phar://)
  • containerised / immutable deploys where paths in traces don't exist on the machine rendering them (build artefacts, opcache.preload, stripped sources)
  • generated or compiled files, and very long or minified lines

Design so a missing snippet degrades gracefully rather than throwing inside the formatter (see recursion, below), and don't build alerting logic that assumes the snippet is present.

Source snippets are source code. If the renderer quotes the failing lines, it can print secrets that live in code — hardcoded keys, DSNs, internal hostnames, feature flags. Keep snippets behind the debug flag and confirm they're suppressed in production mode, same as traces.

Output before output. Any formatter that echoes directly breaks under ob_start(), breaks after headers_sent(), and breaks inside a partially-rendered Blade view. Prefer a formatter that returns a string and let the caller decide how to emit it. If the package only echoes, wrap the call and capture with ob_start().

Escape before you render. An HTML formatter interpolating $e->getMessage(), file paths, and quoted source lines is an XSS vector when the message contains user input. Confirm htmlspecialchars(..., ENT_QUOTES, 'UTF-8') is applied to every interpolated value; wrap your own formatter in an escaping helper.

Debug flag discipline. Traces leak absolute paths, dependency names, and sometimes credentials in DSNs; snippets leak code. Default to off, enable only via an explicit env var, and add a test that fails if trace (or source context) appears in production-mode output.

Guard against recursion. If the handler itself throws (bad formatter, missing class, unreadable source file during snippet extraction, OOM during trace rendering), you get an infinite loop or a blank page. Wrap the formatting call in try/catch and fall back to a one-line native message.

Treat every 0.x minor as potentially breaking. v0.5.0 happened to be a fix, but 0.x reserves the right to change signatures and behaviour at 0.6, 0.7, and so on. Mitigations: pin "slick/error-handler": "0.5.*", keep your formatters in your own namespace so swapping to Whoops/Ignition is a one-line change, and copy rather than extend internals. The formatter interface is the only part worth depending on deeply.

Where the package genuinely wins. If you need a small, dependency-light way to render throwables in a chosen format outside a full framework, it's a reasonable fit — Laravel's Ignition is not usable standalone. If you're already in Laravel and only need better web traces, Ignition/Whoops are better supported; use this package for CLI and bootstrap-level formatting instead.

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.
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
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console