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
) {
Copied!

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' fromArray() normalisation threw PHP's global \InvalidArgumentException, a different class from nr_llm's own Exception\InvalidArgumentException — the first entry in the list above exists only because of that mismatch (nr_ai_search's NrLlmChatClient documents exactly this trap).

Decision 

  • Netresearch\NrLlm\Exception\NrLlmExceptionInterface (extending \Throwable) marks every exception this extension throws on its public API surface. The five core exceptions and ProviderException (which its five subtypes inherit from) implement it.
  • ChatMessage / ToolSpec / ToolCall normalisation errors now throw Exception\InvalidArgumentException instead of PHP's global class. Backwards compatible: the nr_llm class extends \InvalidArgumentException, so existing catches keep matching.
  • A reflection test sweeps both exception directories so a future exception class cannot ship without the marker.

Consumers can now write catch (NrLlmExceptionInterface $e) — one arm, future-proof.

Consequences 

  • Remaining classes that import the global InvalidArgumentException for their own validation errors (response parsers, task readers, backend response DTOs) are unchanged — they are not part of the chat/embedding consumer surface. Migrating them onto Exception\InvalidArgumentException is compatible follow-up work, guarded by the same reflection test.
  • Catch-all remains opt-in: consumers that want to handle budget exhaustion differently from provider outages keep catching the concrete classes.