PAIPAI
Error Handling

Client Errors

Handle expected domain errors, HTTP failures, and durable background run failures.

Client code sees three distinct forms of failure.

Expected Domain Errors

Portable domain errors retain their typed classes across direct and HTTP transports:

import { ThreadBusyError, ThreadConflictError } from "@pai/client";

try {
  await thread.send("Continue");
} catch (error) {
  if (error instanceof ThreadBusyError) {
    await thread.send("Continue", { queue: { mode: "queue" } });
  } else if (error instanceof ThreadConflictError) {
    await thread.refresh();
  } else {
    throw error;
  }
}

Use the class and its fields rather than parsing its message.

HTTP Request And Stream Failures

@pai/client-http throws PaiHttpError for a non-success response or established SSE stream error that is not one of the portable domain classes:

import { PaiHttpError } from "@pai/client-http";

try {
  await thread.stop();
} catch (error) {
  if (!(error instanceof PaiHttpError)) throw error;

  showError({
    message: error.message,
    reference: error.errorId,
    canRetry: error.retryable,
  });
}

The error exposes:

  • phase: "response" or "stream";
  • status: the HTTP status when a response exists;
  • code: a stable public code when provided;
  • errorId: an optional diagnostic reference; and
  • retryable: optional safe retry guidance.

Known domain response DTOs are reconstructed before the generic HTTP fallback. Network and response-decoding failures become a generic PaiHttpError with phase: "response". SSE read, decoding, and malformed-event failures become a generic PaiHttpError with phase: "stream". This prevents transport or provider details from becoming accidental UI text. Caller abort reasons, existing PaiHttpError instances, and portable typed domain errors remain unchanged.

Safe code, message, errorId, and retryable metadata from unexpected server failures is accepted only from PAI's versioned error DTO. Similar-looking payloads from a proxy or other infrastructure receive the generic safe fallback.

Background Run Failures

thread.send() can succeed because the run was admitted and then fail later while the model, a tool, or runtime work is executing. That is not a failed HTTP request. PAI persists a safe ThreadState.error and carries it through refresh and watch updates:

const run = await thread.send("Create the report");
const state = await run.waitUntilIdle();

if (state.error?.runId === run.runId) {
  showError({
    message: state.error.message,
    reference: state.error.errorId,
    canRetry: state.error.retryable,
  });
}

The next accepted run clears the previous thread error. The failed run remains the durable history record.

In React, use chat.state.error for this durable failure:

function RunErrorNotice() {
  const chat = AssistantAI.useChat();
  const error = chat.state.error;

  if (!error) return null;

  return (
    <div role="alert">
      <p>{error.message}</p>
      {error.errorId ? <code>Reference: {error.errorId}</code> : null}
    </div>
  );
}

The React controller's chat.error instead represents a local command or observation rejection, such as a failed send() request. Keep the two paths separate when deciding whether to retry a command or retry a failed run.

Error Record Safety

PaiErrorDetails contains deliberate client-facing details:

type PaiErrorDetails = {
  code: string;
  message: string;
  retryable?: boolean;
};

PersistedErrorRecord adds the optional opaque errorId. It does not contain an original stack, arbitrary cause, provider payload, or internal exception fields. Treat codes as stable branch keys and messages as display text.

Do not use an errorId as authorization or expose a browser-accessible error lookup endpoint. Send it to support or your own client logging system as a correlation value.

Error Code Ownership

PAI owns its framework-authored codes. Applications own every code returned by their runtime mapError and receiver mapHttpError functions and should keep those meanings stable for their clients. Do not branch on display messages.

The built-in safe fallback and control-flow codes are:

CodeMeaning
run_failedAn unclassified background run failure
run_interruptedRecovery found a run whose worker could no longer complete it
run_rejectedA lifecycle hook deliberately rejected a run with a safe message
tool_failedAn unclassified tool or tool-lifecycle failure
tool_cancelledA tool call was cancelled or abandoned
tool_disabledA configured tool was unavailable for this context
tool_not_foundA requested tool was not present
tool_rejectedA lifecycle hook deliberately rejected a tool call
internal_errorA generic safe fallback for internal persisted error state
internal_server_errorAn unexpected HTTP request failure
internal_stream_errorAn unexpected established-stream failure
file_provider_unavailableThe receiver has no file provider for the requested operation

Portable typed thread errors, such as thread_not_found and thread_conflict, are documented in the client error reference. Applications can use their own naming convention for mapped codes; a product prefix is useful when it needs to distinguish its catalogue from PAI's.

mock_error and fixture_tool_error are deterministic test-fixture codes, not production runtime classifications.

On this page