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; andretryable: 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:
| Code | Meaning |
|---|---|
run_failed | An unclassified background run failure |
run_interrupted | Recovery found a run whose worker could no longer complete it |
run_rejected | A lifecycle hook deliberately rejected a run with a safe message |
tool_failed | An unclassified tool or tool-lifecycle failure |
tool_cancelled | A tool call was cancelled or abandoned |
tool_disabled | A configured tool was unavailable for this context |
tool_not_found | A requested tool was not present |
tool_rejected | A lifecycle hook deliberately rejected a tool call |
internal_error | A generic safe fallback for internal persisted error state |
internal_server_error | An unexpected HTTP request failure |
internal_stream_error | An unexpected established-stream failure |
file_provider_unavailable | The 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.