---
title: "ADR-053: One marker interface for all thrown exceptions"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-053@0.35"
source: "Adr/Adr053ExceptionMarkerInterface.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# 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' `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

-   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.
