Skip to main content
chat.agent errors fall into four layers, each with different recovery semantics. The default behavior is conversation-preserving: a thrown error in a hook or run() does not kill the chat. The current turn ends with an error chunk, and the agent waits for the user’s next message.

Error layers at a glance

Stream errors mid-turn

When the model API errors mid-response (rate limits, network failures, malformed output), the AI SDK’s streamText calls the onError callback. Use uiMessageStreamOptions.onError to convert the error to a user-friendly string. The string is sent to the frontend as an error chunk.
Returning a string from onError is what gets shown to the user. Do not return raw error messages — they may leak internal details (API keys, stack traces, etc.).
The frontend receives this as an error chunk that useChat exposes via its error field:

Hook and turn errors

If any lifecycle hook (onValidateMessages, onChatStart, onTurnStart, hydrateMessages, onAction, prepareMessages, onBeforeTurnComplete, onTurnComplete) or run() throws an unhandled exception, the turn loop catches it:
  1. Writes { type: "error", errorText: error.message } to the stream
  2. Writes a turn-complete chunk to close the turn
  3. Waits for the next user message
The conversation stays alive. The user can send another message and continue.

Catching errors in your own hooks

For granular control, wrap your hook code in try/catch and decide what to do. Common patterns:
The Error.message you throw is sent verbatim to the frontend as the error chunk’s errorText. Use messages safe for end users.

Catching errors inside run()

Catch errors from work you await inside run(), such as loading application data. Errors emitted after streamText() returns reach the response stream instead. Record those in onTurnComplete, which receives the partial response and the error.

Saving error state to your DB

To persist errors for debugging or undo, use onTurnComplete (which fires even after errors) or the standard task onComplete hook.

Using onTurnComplete

onTurnComplete fires after every turn — successful or errored. On an errored turn responseMessage is undefined or partial and error carries the thrown value (with finishReason set to "error"). Use this to mark the turn as failed:

Using the standard onFailure task hook

For run-level failures (the entire run dies), use the standard task onFailure hook. This fires when the run terminates with an unhandled exception:
chat.agent defaults to one run attempt. Setting oomMachine enables one additional attempt on the larger machine after an out-of-memory failure. Recover from a failed turn by sending another message or calling regenerate().

Recovery patterns

Pattern 1: Undo to last successful response

A common pattern is to let the user “undo” the failed turn and try again. Combine chat.history.rollbackTo with a custom action:
On the frontend, show an “Undo” button when an error occurs:
app/chat/UndoButton.tsx

Pattern 2: Retry the last message

For transient errors (network blips, rate limits), the simplest recovery is to re-send the last user message. The AI SDK’s useChat provides regenerate():
regenerate() removes the last assistant response and re-sends. If your storage implements loadContext, handle trigger === "regenerate-message" there: remove trailing assistant messages from the active branch so the model receives the last user message as its prompt.

Pattern 3: Save partial responses

When a stream fails or you stop a response, transcript storage preserves the partial assistant message with final: false. Keep that flag in your storage adapter so a reload doesn’t present an incomplete answer as finished. Use onTurnComplete to record the outcome. Check error or stopped before classifying a response as partial:
trigger/chat.ts

Pattern 4: Retry with another model

streamText() returns before the provider finishes streaming. A try/catch around that call only catches setup failures; it doesn’t catch errors that arrive later in the stream. For a streamed provider failure, let the failed turn finish, choose a fallback model on your server, then regenerate the response. The partial response remains available until you replace it. Check whether tools already performed side effects before retrying, and make those operations idempotent.

What gets written to the stream on error

When an error occurs at any layer, the frontend’s UIMessageChunk stream surfaces an error chunk:
A turn-complete control record follows on session.out (header-form, not a data chunk — see turn-complete control record for the wire format) to mark the turn as done. The AI SDK’s useChat processes this and:
  1. Sets useChat’s error field to an Error with message = errorText
  2. Calls the user’s onError callback (if set)
  3. Sets status to "error". Sending another message or regenerating starts a new request.

Frontend error handling

Showing the error to the user

Distinguishing error types

The errorText is just a string, so distinguish error types via prefixes or codes:
For richer error structures, use chat.response.write() with a custom data-error part type. This lets you ship structured error metadata (codes, retry hints, etc.) instead of stringly-typed messages.

Errors from accessToken / startSession

If your accessToken or startSession callback throws (auth failure, DB write failure, network error), the rejection surfaces through useChat’s error state — same as a stream error. The transport doesn’t retry the callback automatically; the customer is responsible for handling it.
startSession failures most commonly mean the customer’s authorization layer rejected the request (no plan, quota exceeded, user not allowed to chat with this agent). The customer’s server should produce a meaningful error message; the transport propagates it verbatim to useChat’s error state.

Run-level retries

chat.agent defaults to retry: { maxAttempts: 1 }. Turn errors are handled within the conversation, so another message can succeed in the same run. If you configure oomMachine, the agent uses up to 2 attempts and retries an out-of-memory failure on that machine. This doesn’t enable retries for ordinary provider or tool errors. Use the AI SDK’s provider retry settings for transient model-call failures. After a turn has failed, let the user send another message or call regenerate(). Retrying a conversation can repeat tool side effects, so use idempotency keys for writes.

Best practices

  1. Always set uiMessageStreamOptions.onError to sanitize stream errors before they reach the user.
  2. Persist messages in onTurnStart so a mid-stream failure still leaves the user’s message visible.
  3. Use onTurnComplete to mark turn status in your DB (ok / errored / stopped).
  4. Don’t throw raw errors with internal details in hooks — catch, log, then throw a sanitized user-facing message.
  5. Provide an undo or retry affordance in the UI when errors occur.
  6. Use onFailure for run-level monitoring (Sentry, monitoring dashboards).
  7. For known transient errors (rate limits, network), consider a fallback model inside run() instead of failing the turn.

ChatChunkTooLargeError

A specific run-failing error worth flagging on its own. Anything written through the chat output is one record on the underlying realtime stream, capped at ~1 MiB per record. A single chunk over the cap throws ChatChunkTooLargeError (named export from @trigger.dev/sdk). The most common trigger is a tool whose result object is large enough to overflow as one tool-output-available chunk. The error carries chunkType, chunkSize, and maxSize. Catch with the isChatChunkTooLargeError guard and route oversized values out-of-band. See Large payloads in chat.agent for the ID-reference pattern that works around the cap, plus guidance on transient data parts and out-of-band logging.

See also