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.
Prerequisites
Section titled “Prerequisites”The ChatClient from Create direct conversations. The full list of codes is in the error reference.
-
Catch typed errors. Every error carries
.status(the HTTP status) and.code(the API’serrorstring, for exampletarget_user_not_eligible)..bodyholds the whole error body. A failed token request surfaces the same way.Class Status Typical codes ChatUnauthorizedError401 invalid_tokenChatForbiddenError403 insufficient_scope,missing_capability,client_context_mismatchChatNotFoundError404 not_foundChatUnprocessableError422 target_user_not_eligible,conversation_archived,actor_not_memberChatRateLimitedError429 rate_limitedChatApiErroranything else, including 400 and 5xx invalid_request,server_errorEvery class extends
ChatApiError, so onecatchfor it covers them all. -
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. -
Retry only what is safe to retry. See the policy below, and the helper after it.
Retry policy
Section titled “Retry policy”| 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.
Idempotent create
Section titled “Idempotent create”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. |
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); } }}Verify
Section titled “Verify”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.
Troubleshooting
Section titled “Troubleshooting”| 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. |