Page actions

Error Handling

When a run fails, @threadplane/chat surfaces a structured AgentError on the agent's error signal instead of a bare Error. It carries a machine-readable failure kind, a retryable flag, an optional HTTP status, and the original cause — so you can render cause-specific copy, decide whether to offer a retry, and keep the raw error for telemetry. An interrupted error carries two more fields, recovery and detail, which say which action the adapter can justify offering.

Note: Zero-config by default

<chat> already renders the built-in <chat-error> primitive, which shows legible per-kind copy and the one action the error supports — a Retry button, a Check status button, or neither. You only need this guide when you want custom error UI.

The AgentError shape

agent.error() returns an AgentError | undefined. AgentError extends Error, so existing .message / instanceof Error reads keep working:

import { AgentError, type AgentErrorKind, type AgentRecovery } from '@threadplane/chat';
 
class AgentError extends Error {
  readonly kind: AgentErrorKind;   // 'connection' | 'auth' | 'server' | 'interrupted' | 'aborted'
  readonly retryable: boolean;     // could retrying the same request plausibly succeed?
  readonly status?: number;        // HTTP status, when the failure came from a response
  readonly cause: unknown;         // the original raw error, preserved for debugging and diagnostics
  readonly recovery?: AgentRecovery; // 'retry' | 'check' | 'none' — set on 'interrupted' only
  readonly detail?: string;        // the sentence to show when recovery is 'check' or 'none'
}

Failure kinds

kindCauseretryable
connectionOffline / DNS / connection refused / fetch failedtrue
auth401 / 403 — credentials or API key are wrongfalse
server5xx (retryable) or a non-auth 4xx like 400/404/429 (not retryable)varies
interruptedThe stream closed mid-response after a run had startedonly when recovery is retry
abortedThe user pressed stop — treated as a graceful idle, not surfaced as an errorfalse

The retryable flag is the value to branch on for UI: connection and server (5xx) are retryable; auth, aborted, and non-auth 4xx are not. An interrupted error is retryable only when its recovery is retry, because replaying a request the server may already have received could duplicate a tool call or a booking.

Recovering from an interruption

A stream that closes without terminal evidence settles as interrupted rather than as a completed turn. Partial content and approval state are preserved in every case; what differs is the action the adapter can prove is safe, which it reports on recovery.

recoveryThe adapter knowsretryableWhat to render
retryThe request was never dispatched, so re-running it cannot duplicate server-side worktrueA Retry button calling agent.retry()
checkThe outcome is uncertain, and the backend supports a read-only status checkfalseA Check status button calling agent.checkStatus?.()
noneThe outcome is uncertain and nothing can verify itfalseThe detail sentence, and no action

detail is set whenever recovery is check or none, and reads as a continuation of message. Show both: it is the only thing left to say when the outcome cannot be confirmed, or when the agent exposes no checkStatus to offer.

checkStatus() is the optional read-only reconciliation on the Agent contract. It asks the backend what happened; it never resubmits the request and never appends a message. Its answer arrives on the signals rather than in the return value — a run the backend reports as finished clears error and returns status to idle, and any messages it committed appear on messages. An outcome that stays unknown leaves the error in place, so the control can be offered again. Adapters expose the member only when their transport can answer, so feature-detect it before rendering the button.

Connections that stay open but stop producing events are a different problem. They need timeout rules of your own; nothing here classifies them.

Reading the error

const err = agent.error();          // AgentError | undefined
if (err) {
  console.warn(err.message);        // legible, per-kind copy
  if (err.kind === 'auth') showApiKeyHelp();
  if (err.recovery === 'check' && agent.checkStatus) showCheckStatusButton();
  else if (err.retryable) showRetryButton();   // → agent.retry()
}

agent.retry() re-runs the last request and clears error. It is a no-op when a run is in flight or there is nothing to retry.

The built-in error UI

<chat> auto-renders <chat-error>, which reads the agent's error and shows the message plus at most one action:

<!-- Rendered automatically inside <chat>; use it standalone to place your own -->
<chat-error [agent]="chatRef" />

It renders a Check status button when recovery is check and the bound agent exposes checkStatus, a Retry button when the error is retryable, and otherwise the message alone. Both buttons call the bound Agent's method for you, and detail is rendered under the message whenever it is set. For non-retryable failures (e.g. auth), no button is shown — retrying the same bad credential would just fail again.

Custom error UI

Read agent.error() directly to render your own surface. Override the default copy by mapping error.kind to your own strings — AGENT_ERROR_MESSAGES holds the built-in defaults:

import { Component } from '@angular/core';
import { injectAgent } from '@threadplane/langgraph';
import { AGENT_ERROR_MESSAGES, type AgentErrorKind } from '@threadplane/chat';
 
const COPY: Record<AgentErrorKind, string> = {
  ...AGENT_ERROR_MESSAGES,
  auth: 'Your API key looks wrong — update it in Settings.',
};
 
@Component({
  selector: 'app-chat',
  template: `
    @if (agent.error(); as err) {
      <div role="alert" class="my-error">
        <span>{{ err.kind === 'interrupted' ? err.message : COPY[err.kind] }}</span>
        @if (err.detail) {
          <span>{{ err.detail }}</span>
        }
        @if (err.recovery === 'check' && agent.checkStatus) {
          <button (click)="agent.checkStatus!()">Check status</button>
        } @else if (err.retryable) {
          <button (click)="agent.retry()">Try again</button>
        }
      </div>
    }
  `,
})
export class AppChatComponent {
  readonly agent = injectAgent();
  protected readonly COPY = COPY;
}

The interrupted entry in AGENT_ERROR_MESSAGES is only the fallback for an error built without a recovery value, so the example above prefers err.message for that kind: the adapter has already set it to the copy that matches the recovery it chose. AGENT_RECOVERY_MESSAGES and AGENT_RECOVERY_DETAILS hold those two tables if you would rather read them by recovery yourself.

extractErrorMessage(value) is also exported for coercing any unknown error value into a readable string when you render an error outside <chat-error>.

Classifying errors in a custom backend

Runtime adapters normalize raw failures through toAgentError() before setting agent.error(). If you write a custom adapter, call it too — or throw an AgentError directly:

import { toAgentError } from '@threadplane/chat';
 
try {
  await runMyBackend();
} catch (raw) {
  // Classifies by structured status → connection markers → HTTP-shaped message → server fallback.
  // Idempotent: an existing AgentError passes through unchanged. User aborts settle to 'aborted'.
  this.setError(toAgentError(raw));
}

isAbortError(raw) is the shared predicate both adapters and toAgentError() use to recognize a user-requested stop (AbortError) so it settles to idle rather than surfacing as an error.

API reference

import {
  AgentError,
  type AgentErrorKind,
  type AgentRecovery,
  AGENT_ERROR_MESSAGES,
  AGENT_RECOVERY_MESSAGES,
  AGENT_RECOVERY_DETAILS,
  toAgentError,
  isAbortError,
  ChatErrorComponent,
  extractErrorMessage,
} from '@threadplane/chat';
ExportPurpose
AgentErrorStructured error class on agent.error() (kind, retryable, status?, cause, recovery?, detail?)
AgentErrorKindUnion of the five failure classes
AgentRecoveryUnion of the three recovery actions: retry, check, none
AGENT_ERROR_MESSAGESDefault human-facing copy per kind
AGENT_RECOVERY_MESSAGESHuman-facing copy per recovery action, for interrupted errors
AGENT_RECOVERY_DETAILSThe second line of the interrupted banner, per recovery action
toAgentError(raw)Classify any raw value into an AgentError (idempotent)
isAbortError(raw)Predicate: is this a user-requested stop?
ChatErrorComponentThe <chat-error> primitive (message, optional detail, and at most one action)
extractErrorMessage(value)Coerce any error value into a readable string

What's next

Looking for something specific?