---
title: "ADR-053: One marker interface for all thrown exceptions"
manual: "LLM Foundation"
version: "0.41"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-053@0.41"
source: "Adr/Adr053ExceptionMarkerInterface.rst"
rendered: "2026-10-20T08:48:47+00:00"
---

# ADR-053: One marker interface for all thrown exceptions {#adr-053}

-   *Status:* Accepted
-   *Date:* 2026-07-12
-   *Authors:* Netresearch DTT GmbH

## Context {#adr-053-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' `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 {#adr-053-decision}

-   `Netresearch\NrLlm\Exception\NrLlmExceptionInterface` (extending
    `\Throwable`) marks every exception this extension throws on its
    public API surface. Core, provider, agent, specialized, skill and tool
    exception hierarchies implement it. The compatible
    `Exception\RuntimeException` and `Exception\LogicException`
    subclasses cover internally generated errors without a more specific
    failure type; the existing `Exception\InvalidArgumentException`
    covers validation, including delegated credentials and editor actions.
-   `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 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 (NrLlmExceptionInterface $e)` 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 {#adr-053-consequences}

-   The remaining classes that imported the global
    `InvalidArgumentException` for their own validation errors
    (response parsers, task readers, backend response DTOs, value
    objects) throw `Exception\InvalidArgumentException` now — the
    compatible follow-up named here is done, guarded by the same
    reflection test. One deliberate exception:
    `Service\Task\TaskInputResolver` keeps the global import because it
    only *catches* the exception around `RecordTableReader::fetchAll()`
    — narrowing that catch to the nr_llm subclass would miss a plain
    `\InvalidArgumentException` raised by third-party code inside the
    read path.
-   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) {#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](https://docs.typo3.org/permalink/netresearch/nr-llm:developer-streaming@0.41) for the pre-first-chunk fallback boundary.
