maduser/argon-error is the HTTP error-handling layer for Argon applications.
It turns uncaught throwables into PSR-7 responses, lets applications register
exception-specific reporting and rendering policies, and integrates with the
shared Argon runtime error-handler contract.
The package stays intentionally small:
ErrorHandlerbridges Argon runtime failures to the dispatcher/formatter stack.ExceptionDispatcherruns registered exception policies before falling back to the formatter.ExceptionFormattercreates JSON or plain-text PSR-7 responses.ErrorHandlerServiceProviderwires the package into anArgonContainer.
composer require maduser/argon-errorThe formatter depends on PSR-17 response and stream factories. In a full Argon
stack those are normally provided by maduser/argon-http-message.
Register the provider during application boot:
use Maduser\Argon\Container\ArgonContainer;
use Maduser\Argon\Error\Provider\ErrorHandlerServiceProvider;
$container = new ArgonContainer();
$container->register(ErrorHandlerServiceProvider::class);
$container->boot();The provider binds:
Maduser\Argon\Support\Contracts\ErrorHandlerInterfaceMaduser\Argon\Error\Contracts\ExceptionDispatcherInterfaceMaduser\Argon\Error\Contracts\ExceptionFormatterInterfaceMaduser\Argon\Error\Contracts\ExceptionPolicyRegistryInterface
Any service tagged as ExceptionPolicyInterface can register custom exception
reporting and rendering during container boot.
use App\Http\AppExceptionPolicy;
use Maduser\Argon\Error\Contracts\ExceptionPolicyInterface;
$container->set(AppExceptionPolicy::class)->tag(ExceptionPolicyInterface::class);Policies separate side effects from response creation:
- reporters run first for all matching exception types;
- renderers run after reporters and may return a
ResponseInterface; - a renderer returning
nulllets the dispatcher continue; - if no renderer returns a response, the formatter creates the fallback response.
use Maduser\Argon\Error\Contracts\ExceptionPolicyInterface;
use Maduser\Argon\Error\Contracts\ExceptionPolicyRegistryInterface;
use App\Exceptions\PaymentFailed;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Throwable;
final readonly class AppExceptionPolicy implements ExceptionPolicyInterface
{
public function __construct(
private LoggerInterface $logger,
private ResponseFactoryInterface $responses,
private StreamFactoryInterface $streams,
) {
}
public function register(ExceptionPolicyRegistryInterface $exceptions): void
{
$exceptions->report(
Throwable::class,
fn(Throwable $e, ServerRequestInterface $request): void => $this->logger->error(
$e->getMessage(),
['exception' => $e, 'path' => $request->getUri()->getPath()]
)
);
$exceptions->report(
PaymentFailed::class,
fn(PaymentFailed $e): void => $this->notifyBillingChannel($e)
);
$exceptions->render(
RuntimeException::class,
fn(RuntimeException $e): ?ResponseInterface => $this->responses
->createResponse(500)
->withHeader('Content-Type', 'application/json')
->withBody($this->streams->createStream('{"error":"Runtime failure"}'))
);
}
}Renderer selection is deterministic. More specific exception classes win before parent classes or interfaces. If two renderers are registered for the same specificity, registration order wins.
Reporter and renderer failures are logged and swallowed. Exception handling must not fail because an application callback failed.
ExceptionFormatter uses the request Accept header:
application/jsonreturns a JSON error payload.- anything else returns
text/plain.
HTTP status resolution is deliberately conservative:
- exceptions implementing
HttpExceptionInterfacemay provide an explicit status code; - otherwise throwable codes in the
400..599range are used; - invalid or non-HTTP codes fall back to
500.
Stack traces are hidden by default. They are included only when debug mode is
enabled or the exception implements SafeToDisplayExceptionInterface.