This guide describes the API and behavior changes when upgrading from 2.x to 3.x.
Use the Interop service provider for container integration
without changing the existing package or namespace. The short bootstrap is
container($application, ErrorHandlerServiceProvider::class) for external
dependencies and normal application overrides. When a required interface's
construction failure must prevent fallback to the library default, merge
definitions with AggregateServiceProvider, library first and application last. Registration remains explicit;
use factory services for fresh/nested scopes because container lookups are cached.
The Interop provider is the single container integration entrypoint.
The Laminas ConfigProvider adapter and standalone ErrorHandlerContainer
have been removed in favor of FastForward Container with this provider.
-
Upgrade the application to PHP
^8.3; CI targets PHP 8.3–8.5. Resolve Composer dependencies again. The library does not commitcomposer.lock; resolve development dependencies for your PHP version withcomposer update --no-plugins --no-scripts. Applications keep their own locks. -
Expect exit status 1 after a globally uncaught
Throwableis reported successfully. Version 2 could exit with status 0. A reporter that explicitly terminates the process still controls its own exit code. DirecthandleThrowable()calls do not exit. -
Pair
register()andunregister()in reverse order. A repeatedregister()on the same instance is idempotent, not reference counted. Out-of-order unregistration now throwsLogicExceptioninstead of silently removing someone else's callback. Use separate instances for nested middleware and do not pre-register the middleware's own instance. -
A reporter must not call its own
ErrorHandlerrecursively. After a reporter throws, that instance stops retrying it until it is unregistered and registered again. The failure propagates to the caller or PHP; it is not silently swallowed. Only the active registration receives shutdown notifications, preventing duplicates across nested handlers. -
Review custom HTTP rendering. The provider defaults to request-aware pure rendering: JSON or Symfony HTML when accepted, and a generic fallback otherwise, independent of reporter output. In the explicit output-capture adapter, empty output now produces
Internal Server Errorinstead of a stringified exception and stack trace; the string0is preserved. Exception messages no longer become HTTP reason phrases. Codes 400–599 map to their matching status; other integer codes and string SQLSTATE codes map to 500. -
Compatible 2.x maintenance already removed Docker/Compose and consolidated bounded process fixtures under PHPUnit. Keep those Composer/PHPUnit checks; CI supplies the PHP 8.3–8.5 development matrix. No application runtime image is supplied.
-
Replace
new Container\ErrorHandlerContainer($parent)withFastForward\Container\container($parent, ErrorHandlerServiceProvider::class)when the parent only supplies external PSR dependencies. For application overrides, use the merged provider composition above. FastForward is now a runtime dependency, including its transitive packages; consumers resolve them through Composer. Do not rely on the removed package-specificContainer\Exception\ContainerExceptionorNotFoundException; catch the corresponding PSR-11 exception interfaces instead. -
Replace removed constructor-only factories as follows. Names below are relative to
CoiSA\ErrorHandlerexcept the FastForward factory classes.
| Removed API | Replacement |
|---|---|
Container\Factory\AliasFactory |
FastForward\Container\Factory\AliasFactory::get($serviceId) |
Container\Factory\DispatchThrowableHandlerFactory |
Autowire Handler\DispatchThrowableHandler |
Container\Factory\DispatchErrorEventThrowableHandlerFactory |
Autowire Handler\DispatchErrorEventThrowableHandler |
Container\Factory\ThrowableResponseFactoryFactory |
Autowire Http\Message\ThrowableResponseFactory |
Container\Factory\ThrowableStreamFactoryFactory |
Autowire Http\Message\ThrowableStreamFactory |
Container\Factory\ErrorHandlerMiddlewareFactory |
ServiceProvider\Factory\ErrorHandlerMiddlewareFactory, which creates a fresh handler |
Container\ConfigProvider |
ServiceProvider\ErrorHandlerServiceProvider; remove Laminas config-provider discovery and register the Interop provider with FastForward |
Container\Factory\ErrorHandlerFactory |
ServiceProvider\Factory\ErrorHandlerFactory |
Container\Factory\ThrowableHandlerAggregateFactory |
ServiceProvider\Factory\ThrowableHandlerAggregateFactory |
Update factory imports and service identifiers to the new namespaces; no aliases
for the old names are provided. The explicit library factories live under
ServiceProvider\Factory because they encode lifecycle, reporter composition and lazy
response selection. Use the Interop provider with constructor autowiring; a framework-native
configuration adapter is no longer shipped. laminas/laminas-servicemanager is
no longer a development dependency; unrelated transitive Laminas packages may
still be required by FastForward.
Public handler interfaces retain their names and method signatures. The PHP floor, required FastForward dependency, removed container/factories, CLI exit semantics, lifecycle rejection and HTTP output defaults warrant a major release.
The compatible 2.x adapters LogThrowableHandler, FilteringThrowableHandler and
CallablePhpErrorHandler remain available with their existing contracts. Logging
keeps the exact message and reserved exception context; filtering skips only an
explicit delegate; PHP-error callbacks retain four arguments, ignored returns and
application-owned mask policy. They are not automatically discovered or attached
to the default aggregate. See their API contracts.
All three ErrorHandler constructor inputs now also accept callables. Existing
interface instances keep their identity and take precedence over __invoke();
no migration of existing adapters is required. The reporter remains required;
the second and third arguments still default to null and retain the original PHP
error and fatal-shutdown policies. Callable results are ignored, including false
from the four-argument PHP callback. Construction remains inert. See the
complete callback recipe.
Before upgrading a real application, run the documented checks and application-level tests for the actual reporter, middleware stack and PHP SAPI. These isolated fixtures do not establish reliability of third-party loggers, remote services or production infrastructure.
For the built-in handler, failed-reporter suppression applies only during a registered lifecycle. Independent direct calls remain usable after a reporter failure. Middleware propagates a failed reporter instead of retrying it during response rendering. Custom lifecycle implementations retain their own policy. These Throwable boundaries do not recover an application callback's exit/die or native fatal termination. See the complete usage recipes and release workflow.
The middleware constructor now requires ErrorHandlerInterface&ThrowableHandlerInterface.
The built-in ErrorHandler already satisfies this. Custom lifecycle-only implementations
must also implement diagnostic delivery. Middleware reports a downstream Throwable
once, then invokes the response factory; reporter failures propagate without rendering.
The provider's response alias selects NegotiatingThrowableResponseFactory and its generic fallback uses GenericThrowableStreamFactory. Reporter output is not implicitly captured as the response body. HTTP reporters must use private sinks. Implement ThrowableStreamFactoryInterface to return JSON or another public stream directly; the cookbook includes a complete JSON example without echo. ThrowableHandlerInterface remains void, preserving CLI, event and aggregate roles.
ThrowableStreamFactory retains explicit output capture for public templates. Configure a separate public renderer there, rather than a reporter middleware already invoked. Its old concrete service ID no longer controls the default stream alias.
The JSON and optional Whoops response adapters published in 2.5.0 remain available.
Pass their pure renderers directly to middleware. Remove the explicit
ReportingThrowableResponseFactory wrapper used by 2.x middleware, which did not
report independently. The wrapper remains available for other explicit pipelines.
The separate JsonErrorHandlerServiceProvider and WhoopsErrorHandlerServiceProvider
are removed. Use the single ErrorHandlerServiceProvider(bool $debug = false)
with application-first composition:
container($application, ErrorHandlerServiceProvider::class). To enable trusted
development diagnostics, pass new ErrorHandlerServiceProvider(debug: true).
The provider negotiates Accept for every current request; debug never forces HTML.
Symfony renders public/debug HTML by default. Optional Whoops needs an explicit
application override, and JSON-only requests never construct it. Missing/unsupported Accept uses the
generic fallback. JSON and Whoops concrete renderers remain public. Configure
Whoops through the application-overridable PrettyPageHandler service.
Every ThrowableResponseFactoryInterface implementation now requires
createResponseFromThrowable(Throwable $throwable, ServerRequestInterface $request).
Update custom renderers and every direct call to pass the current request.
Middleware forwards it automatically. Stream-factory signatures are unchanged.
A configured page is reused sequentially; middleware scopes, Whoops runs and
response bodies remain distinct. Required overrides may still use one merged
AggregateServiceProvider to prevent fallback to a library interface binding.
Construct direct JSON renderers with the required third SerializerInterface
argument and the debug bool fourth. The provider supplies a Symfony Serializer
with ProblemNormalizer and JsonEncoder. JSON bodies use Problem Details with
type, title, status and generic public detail, replacing the 2.x error
field and nested debug exception object. Debug places the original message in
detail and adds top-level class/trace metadata without argument values or objects.
Update clients and assertions accordingly. Direct JSON returns
application/problem+json; request negotiation also supports application/json
with the same payload. See HTTP details.
The optional Symfony terminal adapter is explicitly selected; normal providers
do not enable it by SAPI detection. With cli: true, the default aggregate adds
it after optional reporters. Keep one exception-rendering owner for Console
applications and choose the exit status in the application bootstrap. See
terminal diagnostics.