ADR-053: One marker interface for all thrown exceptions
- Status
-
Accepted
- Date
-
2026-07-12
- Authors
-
Netresearch DTT GmbH
Context
A consumer that wraps an nr_llm call and wants to convert failures into its own domain exception has to enumerate concrete classes today:
} catch (
InvalidArgumentException // PHP's own, from ChatMessage/ToolSpec::fromArray()
| NrLlmInvalidArgumentException // options validation
| ProviderException // covers the 5 provider subtypes
| BudgetExceededException
| AccessDeniedException
| ConfigurationNotFoundException $e
) {
Two problems. The list goes stale silently: when a future version adds
or rethrows a new exception type, existing catch lists let it escape as
an uncaught 500 instead of the consumer's clean error path. And the
chat/tool value objects' from normalisation threw PHP's
global \Invalid, a different class from nr_llm's
own Exception\ — the first entry in the
list above exists only because of that mismatch (nr_'s
Nr documents exactly this trap).
Decision
Netresearch\(extendingNr Llm\ Exception\ Nr Llm Exception Interface \Throwable) marks every exception this extension throws on its public API surface. Core, provider, agent, specialized, skill and tool exception hierarchies implement it. The compatibleException\andRuntime Exception Exception\subclasses cover internally generated errors without a more specific failure type; the existingLogic Exception Exception\covers validation, including delegated credentials and editor actions.Invalid Argument Exception Chat/Message Tool/Spec Toolnormalisation errors now throwCall Exception\instead of PHP's global class. Backwards compatible: the nr_llm class extendsInvalid Argument Exception \Invalid, so existing catches keep matching.Argument Exception - A reflection test sweeps the complete
Classes/tree, independently of exception names and directories, so a future exception class cannot ship without the marker. A parser-based check also rejects directly constructed native validation, runtime and logic errors in this tree. Consumer-boundary tests verify common catches and native compatibility.
Consumers can write catch for nr_llm-defined
exceptions. Propagated dependency exceptions still need their own catch arm
when the consumer wants to handle them.
The marker covers errors generated by nr_llm. It does not convert exceptions originating in third-party callbacks or dependencies, nor PHP engine errors. Callers of the tool loop handle its approval/input suspension types before the common marker catch to retain their resumable state.
Consequences
- The remaining classes that imported the global
Invalidfor their own validation errors (response parsers, task readers, backend response DTOs, value objects) throwArgument Exception Exception\now — the compatible follow-up named here is done, guarded by the same reflection test. One deliberate exception:Invalid Argument Exception Service\keeps the global import because it only catches the exception aroundTask\ Task Input Resolver Record— narrowing that catch to the nr_llm subclass would miss a plainTable Reader:: fetch All () \Invalidraised by third-party code inside the read path.Argument Exception - Catch-all remains opt-in: consumers that want to handle budget exhaustion differently from provider outages keep catching the concrete classes.
Scope clarification (2026-10-10)
The marker covers exception classes defined by nr_llm. Dependency exceptions that propagate through a public call retain their original class. The bundled streaming adapters, for example, propagate PSR-18 network failures directly. Seven executable adapter contract cases preserve that behavior; they do not claim that the marker catches every dependency failure or every PHP error. See Streaming support for the pre-first-chunk fallback boundary.