Error handling
The typed error hierarchy — ApiError, AuthError, BillingError, RequestTooLargeError — and how to catch, branch on, and format them for the UI.
Every REST call made through createKortix(...) or backendApi rejects with a
real Error subclass — never a plain object. catch it, instanceof it, and
branch on .status/.code. These are the same classes on every host: a
server-side "Kortix as a backend" wrapper and the React UI both import from the
one module.
import { ApiError, BillingError } from '@kortix/sdk';
try {
await kortix.project(projectId).sessions.create();
} catch (err) {
if (err instanceof BillingError) {
// 402 — out of credits / plan limit
} else if (err instanceof ApiError) {
// any other failed request — err.status, err.code, err.detail
} else {
throw err;
}
}The classes
| class | extends | when | key fields |
|---|---|---|---|
ApiError | Error | the default for any failed request — bad status, network failure, timeout, or an aborted request | status, code, details/data, detail, response, url, endpoint, timeout |
AuthError | ApiError | getToken returned null — the request was never sent | code is always 'NO_SESSION' |
BillingError | Error | HTTP 402 — the backend's only billing error today | status (402), detail: { message, ... } |
RequestTooLargeError | Error | HTTP 431 — typically too many files attached to one request | status (431), detail: { message, suggestion } |
BillingError used to be an 8-class hierarchy (AGENT_RUN_LIMIT_EXCEEDED,
THREAD_LIMIT_EXCEEDED, etc.). The backend now only ever returns a plain 402
with { message }, so it's one class — legacy code that still switches on old
error-code strings can delete that branch.
ApiError
The class every failed backendApi/facade call produces. name defaults to
'ApiError' but is overridden for specific failure modes you can match on:
name: 'AbortError',code: 'ABORTED'— the request was aborted externally (navigation, React Query cancellation) — not a timeout, safe to ignore.code: 'TIMEOUT'— the request's own timeout budget elapsed;url,endpoint, andtimeout(ms) are set so you know what timed out.- Otherwise
statusis the HTTP status code andcodeis the backend'serror_code/detail.error_code(falling back to the status as a string) when the body carried one.
message is an enumerable own property (not just inherited from Error),
so spreading the error or JSON.stringify-logging it includes the message —
useful for error reporting.
AuthError
Thrown client-side, before any request goes out, when getToken() resolves to
null — there's no way to sign the request. It's an ApiError subclass, so
err instanceof ApiError still matches; check err instanceof AuthError (or
err.code === 'NO_SESSION') to special-case "not signed in" from a real
backend failure.
BillingError
Thrown for HTTP 402 responses — out of credits, over a plan limit, or any
other billing gate. detail.message is the human-readable reason from the
backend; detail may carry additional fields the backend chose to include.
RequestTooLargeError
Thrown for HTTP 431 (Request Header Fields Too Large) — in practice, this
means too many files were attached to a single request. detail.suggestion
is a ready-to-show hint ("Try uploading files one at a time...").
Helpers
| helper | signature | what |
|---|---|---|
parseBillingError(error) | (error: any) => Error | If error's status is 402, wraps it into a BillingError; otherwise returns error unchanged. Called internally on every 402 response, so callers rarely need it directly. |
isBillingError(error) | (error: any) => boolean | error instanceof BillingError |
formatBillingErrorForUI(error) | (error: any) => BillingErrorUI | null | Returns null for anything that isn't a BillingError; otherwise returns { alertTitle, alertSubtitle } ready to render in an upgrade/pricing modal — with a dedicated "you ran out of credits" copy when the message mentions credits/balance/insufficient. |
import { formatBillingErrorForUI } from '@kortix/sdk';
try {
await kortix.session(projectId, sessionId).start();
} catch (err) {
const ui = formatBillingErrorForUI(err);
if (ui) showUpgradeModal(ui.alertTitle, ui.alertSubtitle);
}In @kortix/sdk/react
@kortix/sdk/react re-exports BillingError, RequestTooLargeError,
parseBillingError, isBillingError, and formatBillingErrorForUI — not
ApiError/AuthError, which stay root-only (@kortix/sdk) since they're the
generic REST-failure shape, not something a chat UI branches on. Import both
from wherever you already import @kortix/sdk/@kortix/sdk/react — same
classes either way.
useSession's send/answerQuestion/answerPermission/rejectQuestion
classify every failure into a KortixSendError (sendError on the hook's
return value) instead of making you instanceof-check by hand:
interface KortixSendError {
kind: 'billing' | 'runtime-not-ready' | 'runtime-error';
message: string; // already formatted for display
billing?: BillingError; // present when kind === 'billing'
cause: unknown; // the original thrown value
}const s = useSession(projectId, sessionId);
if (s.sendError?.kind === 'billing') {
const ui = formatBillingErrorForUI(s.sendError.billing);
// ...
}See React hooks for the rest of useSession's surface.