Skip to content
+

Chat - Events and callbacks

Respond to streaming lifecycle events, tool calls, data chunks, and errors using callback props on ChatProvider and ChatBox.

ChatProvider (and by extension ChatBox) exposes four callback props that fire at key moments in the chat lifecycle. Use them for logging, analytics, side effects, and error handling without modifying the adapter.

Callback overview

Prop When it fires Typical use case
onFinish When a stream reaches a terminal state Analytics, persistence, follow-up UI
onToolCall When a tool invocation state changes Logging, triggering external workflows
onData When a data-* chunk arrives during streaming Transient data, app-level side effects
onError When any runtime error surfaces Error reporting, toast notifications

onFinish, onToolCall, and onData may return a promise — the runtime awaits them, so keep long-running work out of the hot path. onError is fire-and-forget.

The following demo wires all four callbacks to an event log so you can observe their relative timing:

Callback event log

No messages yet

Type a message to get started

Send a message to watch callbacks fire here.

Observing stream completion

The onFinish callback fires when a stream finishes, aborts, disconnects, or errors. This is the primary callback for post-stream side effects.

interface ChatOnFinishPayload {
  message: ChatMessage; // the assistant message
  messages: ChatMessage[]; // all messages after the stream
  isAbort: boolean; // user stopped the stream
  isDisconnect: boolean; // stream disconnected unexpectedly
  isError: boolean; // stream ended with an error
  finishReason?: string; // backend-provided reason
}
<ChatBox
  adapter={adapter}
  onFinish={({ message, messages, isAbort }) => {
    if (!isAbort) {
      // Persist the completed conversation
      saveConversation(messages);
    }
    // Log completion analytics
    analytics.track('chat_response_complete', {
      messageId: message.id,
      partCount: message.parts.length,
    });
  }}
/>

Terminal states

The onFinish callback fires in four scenarios:

Scenario isAbort isDisconnect isError Description
Success false false false Stream completed normally
User abort true false false User clicked the stop button
Disconnect false true true Connection dropped mid-stream
Error false false true Stream ended with an error

A disconnect is also reported as an error: the message is marked as errored and onError fires with code: 'STREAM_ERROR'.

Observing tool invocations

The onToolCall callback fires when a tool invocation state changes during streaming. Use it for side effects outside the message list: logging, analytics, or triggering external workflows.

interface ChatOnToolCallPayload {
  toolCall: ChatToolInvocation | ChatDynamicToolInvocation;
}
<ChatBox
  adapter={adapter}
  onToolCall={({ toolCall }) => {
    console.log(`Tool ${toolCall.toolName}: ${toolCall.state}`);

    if (toolCall.state === 'output-available') {
      // Tool execution completed, trigger follow-up
      analytics.track('tool_executed', { tool: toolCall.toolName });
    }
  }}
/>

Tool invocation states

State Description
input-streaming Tool input is being streamed
input-available Tool input is fully available
approval-requested Waiting for human-in-the-loop approval
approval-responded User has approved or denied
output-available Tool execution completed with output
output-error Tool execution failed
output-denied User denied the tool execution

Receiving data chunks

The onData callback fires when a data-* chunk arrives during streaming. Use it for transient data that should trigger app-level side effects without being persisted in the message.

type ChatOnData = (part: ChatDataMessagePart) => void | Promise<void>;
<ChatBox
  adapter={adapter}
  onData={(part) => {
    if (part.type === 'data-progress') {
      setProgressPercent(part.data.percent);
    }
  }}
/>

Register the data-progress payload in ChatDataPartMap so part.data is typed — see Type augmentation:

declare module '@mui/x-chat/types' {
  interface ChatDataPartMap {
    'data-progress': { percent: number };
  }
}

Use onData for backend-driven UI updates that are transient: progress bars, status indicators, or notifications that shouldn't be stored as message parts.

Handling errors

The onError callback fires when any runtime error surfaces, whether from adapter methods, stream processing, or rendering.

type ChatOnError = (error: ChatError) => void;
<ChatBox
  adapter={adapter}
  onError={(error) => {
    console.error('[Chat error]', error.source, error.message);

    // Show a toast notification
    toast.error(error.message);

    // Report to error tracking
    Sentry.captureException(new Error(error.message), {
      tags: { code: error.code, source: error.source },
      extra: {
        recoverable: error.recoverable,
        retryable: error.retryable,
        ...error.details,
      },
    });
  }}
/>

Error object structure

type ChatErrorCode =
  | 'HISTORY_ERROR'
  | 'SEND_ERROR'
  | 'STREAM_ERROR'
  | 'REALTIME_ERROR'
  | 'REGENERATE_ERROR';

interface ChatError {
  code: ChatErrorCode; // machine-readable error code
  message: string; // human-readable description
  source: ChatErrorSource; // where the error originated
  recoverable: boolean; // whether the runtime can continue
  retryable?: boolean; // whether the failed operation can be retried
  details?: Record<string, unknown>; // additional context
}

type ChatErrorSource = 'send' | 'stream' | 'history' | 'render' | 'adapter';

Error sources

Source When it fires
send sendMessage() rejected
stream Stream processing encountered an error
history listMessages() or listConversations() rejected
render A rendering error occurred in the message list
adapter A generic adapter method threw

Reading errors in components

Prefer the hooks below over onError when you want to render error state in the UI rather than fire a side effect. Errors also surface through hooks, so you can display them in custom UI:

// Via useChat()
const { error } = useChat();

// Via useChatStatus(), lighter weight, no message subscriptions
const { error } = useChatStatus();

Registering callbacks

All callbacks are registered as props on ChatBox or ChatProvider:

// On ChatBox (all-in-one)
<ChatBox
  adapter={adapter}
  onFinish={handleFinish}
  onToolCall={handleToolCall}
  onData={handleData}
  onError={handleError}
/>

// On ChatProvider (custom layout)
<ChatProvider
  adapter={adapter}
  onFinish={handleFinish}
  onToolCall={handleToolCall}
  onData={handleData}
  onError={handleError}
>
  <MyCustomLayout />
</ChatProvider>

Callback identity matters: ChatProvider memoizes its runtime context on these props, so inline arrow functions invalidate it on every render and re-render all chat components. Declare handlers outside the component or wrap them in useCallback.

See also

API

See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.