Skip to content

Handle errors and retries

Decide, for every Chat error your backend can see, whether to fix the request, tell the user, or try again. The ccs-chat-client library turns every failed call into a typed error, so you can branch on the type and on a stable code.

The ChatClient from Create direct conversations. The full list of codes is in the error reference.

  1. Catch typed errors. Every error carries .status (the HTTP status) and .code (the API’s error string, for example target_user_not_eligible). .body holds the whole error body. A failed token request surfaces the same way.

    Class Status Typical codes
    ChatUnauthorizedError 401 invalid_token
    ChatForbiddenError 403 insufficient_scope, missing_capability, client_context_mismatch
    ChatNotFoundError 404 not_found
    ChatUnprocessableError 422 target_user_not_eligible, conversation_archived, actor_not_member
    ChatRateLimitedError 429 rate_limited
    ChatApiError anything else, including 400 and 5xx invalid_request, server_error

    Every class extends ChatApiError, so one catch for it covers them all.

  2. Branch on the code, not the message. The message is for people and can change. Handle your business cases (target_user_not_eligible, conversation_archived) explicitly and let the rest bubble up.

  3. Retry only what is safe to retry. See the policy below, and the helper after it.

Error Retry?
429 rate_limited Yes. Wait retryAfterSeconds (from the Retry-After header), then retry.
5xx: server_error, identity_unavailable Yes, with exponential backoff and a few attempts.
401 invalid_token Once. The client drops its cached machine token and fetches a new one on the next call. If it fails again, your client setup is wrong.
400, 403, 404, 422 No. The request or your setup is wrong, or the state does not allow it. Fix it.
Network failure before a response Retry only if the call is safe to repeat.

ChatRateLimitedError.retryAfterSeconds is null when the header is missing. Then use your backoff. Calls from your backend share one budget: 600 requests per 60 s per client. Spread bulk work instead of bursting.

Chat has no idempotency key. Whether a repeated call is safe depends on the call.

Call Safe to repeat?
Reads: get, list, listMembers, messages.list, users.lookup Yes
conversations.create with type: 'DIRECT' Yes. It dedupes on the two members and returns the existing conversation.
archive, reactivate, update with the same values Yes
addMember, removeMember Yes. Adding an existing member or removing a removed one leaves the same state. Removing a member who is not in the conversation answers 404.
conversations.create for GROUP or CHANNEL No. A retry after an unknown outcome can create a second one. List first, or store the id you got back.
messages.send No. A retry after an unknown outcome can post the message twice.
handle-errors.ts
import {
ChatApiError,
ChatForbiddenError,
ChatNotFoundError,
ChatRateLimitedError,
ChatUnauthorizedError,
ChatUnprocessableError,
} from '@central-code-station/ccs-chat-client';
// Map a Chat error to your own API's response. Branch on .code, not on the message.
export function toHttpStatus(error: unknown): number {
if (error instanceof ChatUnprocessableError && error.code === 'target_user_not_eligible') return 409;
if (error instanceof ChatUnprocessableError) return 422;
if (error instanceof ChatNotFoundError) return 404;
if (error instanceof ChatForbiddenError || error instanceof ChatUnauthorizedError) return 502; // your setup, not the caller
if (error instanceof ChatRateLimitedError) return 503;
return error instanceof ChatApiError ? 502 : 500;
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// Retry only calls that are safe to repeat: reads, DIRECT create, archive, reactivate, remove member.
export async function withRetry<T>(call: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await call();
} catch (error) {
const retryable = error instanceof ChatRateLimitedError || (error instanceof ChatApiError && error.status >= 500);
if (!retryable || attempt >= attempts) throw error;
const waitMs =
error instanceof ChatRateLimitedError && error.retryAfterSeconds !== null
? error.retryAfterSeconds * 1000
: 500 * 2 ** attempt + Math.random() * 250;
await sleep(waitMs);
}
}
}

Make a call you know fails, such as sending to an archived conversation, and check that your code sees a ChatUnprocessableError with code === 'conversation_archived' and does not retry. To test the retry path, point a call at a stub that answers 429 with Retry-After: 1 and check that the helper waits about a second.

Symptom Cause and fix
.code is undefined The response had no JSON body, for example a gateway error. Use .status.
401 on every call The machine token is wrong. Check CHAT_API_RESOURCE (the identifier, not the audience) and the client’s scopes.
429 keeps coming back You exceed the budget. Queue and spread the calls instead of retrying faster.
Duplicate messages after a timeout You retried messages.send. Do not retry it blindly.