PAIPAI

Errors

Safe server errors, chat.error vs state.error, retry.

PAI never sends a stack trace to the browser. The client gets a code, a safe message, and sometimes a diagnostic id for support.

Map exceptions on the server. Render them in the chat. Retry when the run failed.

Server

createPai({
  mapError: (error) => {
    if (error instanceof WorkspaceNotFound) {
      return { code: "workspace_not_found", message: "Workspace not found." };
    }
  },
  onRuntimeError(event) {
    // Original exception — stacks, names. Never sent to the browser.
    reportIncident(event);
  },
});

mapError returning undefined uses PAI's generic fallback. HTTP adapters have the same pair as mapHttpError / onHttpError on createPaiExpressReceiver().

Logging

That is not OpenTelemetry. Traces are Observability. Operational records are logger / logLevel on createPai (receivers inherit them, or pass their own).

Records are content-free: a level, a code, a timestamp, and opaque ids in context. Conversation text, error messages and raw errors are excluded. An error record also says what failed:

FieldHolds
errorChainThe error and its causes, outermost first, each as { type, code?, statusCode? }. It follows cause, and an AI SDK RetryError's last attempt. A chain longer than eight keeps its first seven errors and its deepest cause, and sets errorChainTruncated.
context.errorCodeThe client-safe error's code, such as tool_failed.
context.agentName, context.toolNameThe registered agent and tool, on runtime errors.

A type, code or name that isn't a short identifier is left out, so free text such as a message can't reach the log.

{
  "level": "error",
  "code": "runtime_error",
  "timestamp": 1760505600000,
  "errorChain": [
    { "type": "Error" },
    { "type": "SandboxUnavailableError" },
    { "type": "FetchError", "code": "ECONNRESET" }
  ],
  "context": {
    "source": "tool",
    "operation": "execute",
    "errorId": "PAI-…",
    "errorCode": "tool_failed",
    "agentName": "assistant",
    "toolName": "bash"
  }
}

A logger that forwards only context should forward errorChain and errorChainTruncated too:

createPai({
  logger: ({ level, code, context, errorChain, errorChainTruncated }) => {
    log[level]({ event: code, context, errorChain, errorChainTruncated });
  },
  logLevel: "warn",
});

Omit logger for PAI's default server reporter. In development and tests (NODE_ENV is development or test) it prints the raw event, error included, which is the quickest way to debug. Anywhere else, NODE_ENV unset included, it prints the same content-free record a logger would receive, as one JSON line. Pass false to silence. Default logLevel for a supplied logger is "warn". onWarning is the hook for stored-record and checkpoint warnings; omit it to use the same logger.

A supplied onRuntimeError replaces this reporting for runtime errors and receives the raw event instead, in every environment; onHttpError does the same on a receiver. That event is not content-free. Its error can carry request data: a failed model call's error holds the request body, prompt included. Its publicError is safe to show the user but not necessarily to log, because a thrown tool's own message passes through to publicError.message. To log one without conversation data, log toRuntimeDiagnostic(event) from @pai/core (or toHttpDiagnostic(event) from @pai/receiver-http): the content-free record a configured logger receives, errorChain included.

PAI never lets the AI SDK print a failed model call itself: the SDK's default prints the call's request body, so PAI handles the error through these channels instead.

React error fields

These are the error/loading surfaces on useChat(). isRunning, send, retry, and the rest of the controller are on useChat().

FieldMeaningWhat to show
chat.loadingFirst snapshot not hereSkeleton
chat.state.errorDurable failed runSafe message + Retry
chat.errorLocal send/stop threwToast, then Retry if a run exists
chat.watchErrorLive stream is dead“Reconnecting failed” — not a model failure
function ErrorBar() {
  const chat = Assistant.useChat();
  const failed = chat.state.error;

  if (!failed && !chat.watchError) return null;

  return (
    <div>
      <p>{failed?.message ?? "Lost the live connection."}</p>
      {failed ? (
        <button type="button" onClick={() => void chat.retry()}>
          Retry
        </button>
      ) : null}
    </div>
  );
}

retry() re-runs the failed turn. regenerate(...) re-runs from a chosen assistant message. If retry is not available, the client throws ThreadRetryUnavailableError.

Stop is not an error: chat.stop() cancels the active run. Tools must honor ctx.signal.

Next

On this page