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

yiisoft/error-handler

Yii error handler for PHP applications that converts errors and exceptions into PSR-7 responses. It supports debug and production modes, HTML and JSON output, logging, and PSR-15 middleware for consistent error handling.

View on GitHub
Deep Wiki
Context7

Getting Started

yiisoft/error-handler is not an error-logging library and not a Laravel exception handler. It is a PSR-15 middleware that catches any Throwable escaping your middleware stack and converts it into a PSR-7 response, choosing an HTML / JSON / XML / plain-text body based on the request's Accept header. Think of it as App\Exceptions\Handler::render() reimplemented for any PSR-7/PSR-15 application.

composer require yiisoft/error-handler
# you must supply PSR-7/PSR-17 yourself; the package ships no HTTP implementation
composer require nyholm/psr7 psr/log

Look at these files first, in this order:

  1. src/ErrorHandler.php — the PSR-15 middleware (in 0.x it was Middleware/ErrorHandlerMiddleware.php; both names appear in the wild).
  2. src/ThrowableResponseFactory.php — decides renderer + status code, builds the response.
  3. src/Renderer/*.php — HtmlRenderer, JsonRenderer, XmlRenderer, PlainTextRenderer.
  4. src/ThrowableRendererInterface.php — the extension point you will actually implement.
  5. config/ in the repo — the maintained reference DI wiring. Copy that, do not invent your own.

Minimal wiring:

use Psr\Log\LoggerInterface;
use Yiisoft\ErrorHandler\ErrorHandler;
use Yiisoft\ErrorHandler\ThrowableResponseFactory;
use Yiisoft\ErrorHandler\Renderer\{HtmlRenderer, JsonRenderer, PlainTextRenderer};

$factory = new ThrowableResponseFactory(
    renderers: [
        'text/html'        => new HtmlRenderer(),
        'application/json' => new JsonRenderer(),
        'text/plain'       => new PlainTextRenderer(),
    ],
    defaultRenderer: new PlainTextRenderer(),
);

$handler = new ErrorHandler($factory, $container->get(LoggerInterface::class), debug: (bool) $_ENV['APP_DEBUG']);

First use case to get value immediately: a JSON API. Replace Laravel's whoops/HTML error page with a stable application/json envelope, log every Throwable at error level, and return a generic message in production while keeping the full trace behind debug. In Laravel terms this collapses report(), render(), and content negotiation into one middleware. Add it as the outermost middleware of a Slim/Mezzio/Yii3/custom pipeline and you are done.

If you are staying inside Laravel, do not fight the framework — read this package as a design reference for App\Exceptions\Handler, and only install it when you are writing a package or service that must run outside Laravel.

Implementation Patterns

Renderer-per-content-type. The handler does not decide the body; the factory does. Keep one renderer class per media type and register them keyed by exact content type. This is the single most portable idea to steal for Laravel APIs:

final class ProblemJsonRenderer implements ThrowableRendererInterface
{
    public function render(Throwable $e): string
    {
        return json_encode(['type' => 'about:blank', 'title' => 'Internal Server Error'], JSON_THROW_ON_ERROR);
    }

    public function renderVerbose(Throwable $e): string
    {
        return json_encode([
            'title'   => $e->getMessage(),
            'class'   => $e::class,
            'file'    => $e->getFile() . ':' . $e->getLine(),
            'trace'   => explode("\n", $e->getTraceAsString()),
        ], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
    }
}

Register it under application/problem+json (and as the default for /api/* routes, see Gotchas).

Implement both render() and renderVerbose(). render() is the production body and must be safe to show a stranger. renderVerbose() is the debug body. Returning the trace from render() is the most common way people leak stack traces and absolute paths to production.

Status-code mapping via a response-factory decorator. Unlike Laravel, NotFoundHttpException does not automatically become 404 — everything is 500 unless you say otherwise. Wrap the factory:

final class MappingResponseFactory implements ThrowableResponseFactoryInterface
{
    public function __construct(private ThrowableResponseFactoryInterface $inner) {}

    public function createResponse(Throwable $e, ServerRequestInterface $request): ResponseInterface
    {
        $response = $this->inner->createResponse($e, $request);

        $status = match (true) {
            $e instanceof NotFoundException     => 404,
            $e instanceof ValidationException   => 422,
            $e instanceof AuthenticationException => 401,
            $e instanceof DomainException        => 400,
            default                             => $response->getStatusCode(),
        };

        return $response->withStatus($status);
    }
}

This is the direct analogue of Laravel's renderable() / $internalDontReport hooks, and it is where you also add $e instanceof ShouldntReport filtering before the logger sees it.

Environment-driven debug. Wire debug from APP_DEBUG, exactly as you would config('app.debug'). Debug output includes source excerpts; treat it as a local-development feature only.

Logging. Pass a PSR-3 logger and let the handler log every caught Throwable at error. If you also have a Monolog handler or Sentry integration that reports exceptions, guard against double-reporting (e.g. a ShouldntReport interface checked in a wrapper logger).

Middleware placement. Outermost, so it also catches router 404s and DI/container failures that occur after it. In Slim: $app->add(ErrorHandler::class) before routing; in Mezzio: $app->pipe(ErrorHandler::class) as the first pipe.

Testing pattern. Construct a request with an Accept header, pipe a middleware that throws, and assert status, Content-Type, and body:

$handler = new ErrorHandler($factory, new NullLogger(), debug: false);
$response = $handler->process(
    new ServerRequest('GET', '/x')->withHeader('Accept', 'application/json'),
    new class implements RequestHandlerInterface {
        public function handle(ServerRequestInterface $r): ResponseInterface { throw new RuntimeException('boom'); }
    }
);
self::assertSame(500, $response->getStatusCode());
self::assertSame('application/json', $response->getHeaderLine('Content-Type'));

Laravel bridge (only if you must). If you genuinely need PSR-15 error semantics inside Laravel, bind the handler in a service provider and wrap it with symfony/psr-http-message-bridge + nyholm/psr7 inside a Laravel middleware. In practice: convert Illuminate\Http\Request to PSR-7, call process(), convert back. This is viable for shared package code, but it is more moving parts than Laravel's native handler — do it for consistency across services, not for features.

Prettier traces. Pair with yiisoft/error-tracer if you want formatted, collapsed traces in the HTML renderer rather than raw getTraceAsString().

Gotchas and Tips

  • It is not a Laravel package. No service provider, no facades, no Handler class. Laravel's pipeline is not PSR-15; there is no drop-in swap. Decide up front whether you are bridging or just borrowing ideas.
  • Outermost or nothing. If the handler sits below the router, a route-not-found exception propagates past it and PHP renders a fatal error page instead.
  • Fatal errors at boot are out of scope. Parse errors, OOM, and container bootstrap failures happen before the middleware runs. Pair with a shutdown/exception handler (set_exception_handler + register_shutdown_function) and keep display_errors=Off so PHP doesn't emit a second, conflicting body after yours.
  • Accept: */* (curl, many SDKs) can yield HTML. Content negotiation defaults to the configured default renderer. For API route groups, force the JSON renderer instead of relying on the client's header.
  • Exceptions inside a renderer are not caught. Renderers must be pure and dependency-free — no DB lookups, no container resolution, no i18n bootstrapping that could itself throw.
  • Everything is 500 by default. Add the status-mapping decorator above; do not expect Laravel-style HTTP exception magic.
  • Implement render() deliberately. A missing or trace-leaking render() is the #1 production incident with this class of package. Grep your renderers for getTraceAsString, getFile, and getPrevious before shipping.
  • Immutability. PSR-7 withHeader()/withStatus() return new instances. Build the response once, then decorate; don't mutate and discard.
  • Container keys are exact strings. If you resolve renderers from a PSR-11 container by content type, application/problem+json and application/json; charset=utf-8 will not match application/json. Normalise before lookup.
  • API churn across versions. Namespaces and constructor signatures moved (Middleware\ErrorHandlerMiddleware → ErrorHandler, Factory\ThrowableResponseFactory vs top-level). Pin the version and read the changelog before copying snippets from blog posts — including older Yii3 docs.
  • Double logging. The handler logs on catch. If Monolog/Sentry also reports via a global handler, you get duplicates; add a ShouldntReport-style filter in the logger you inject.
  • Headers-already-sent. Once output is flushed you cannot change status or headers. Never echo/var_dump in middleware; keep the error handler as the outermost layer and you stay safe.
  • Bus factor. 22 stars, but it is a Yii3 core package pulled in by yiisoft/yii-web, so it is maintained rather than abandoned. The low opportunity score (4.71) suggests a settled API — few missing features, also few fast fixes. Vendor-pin it and don't expect rapid upstream changes.
  • Where to extend rather than fork: ThrowableRendererInterface for bodies, ThrowableResponseFactoryInterface for status/headers, and a logging decorator for filtering. Those three cover ~95% of what Laravel developers reach for Handler::render()/report()/renderable() to do.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager