Skip to main content
A call that fails throws. This page lists the error classes and their fields, the failures the SDK retries by itself and for how long, the way it uses idempotency keys, and how to cancel a call. Read it before you write the error handling of an integration. The errors of the API, with their codes, are in Errors and rate limits.

Error families

The three families extend Error. APIConnectionError and WebhookSignatureError do not extend APIError: a test with instanceof APIError does not catch them. Two failures throw a built-in error. Invalid client options and an invalid per-call maxRetries throw a plain Error. An invalid file argument of attachments.upload throws TypeError.

Classes by status

The SDK chooses the class from error.status in the response body, which repeats the HTTP status. GoneError is thrown for a 410 in the standard error format. An operation that was removed from the API returns a 410 with another body, which the SDK does not read: the call throws a plain APIError whose status and code are undefined and whose message is empty. Before iSnap removes an operation, it marks it deprecated for at least 30 days: see Versioning and compatibility.

Fields of an error

Every APIError carries these fields: Three points need care:
  • The type declarations give retryAfter and issues as optional. At run time they are null or undefined when they do not apply. Test them with typeof err.retryAfter === 'number' and Array.isArray(err.issues).
  • The type declarations give path as a string. At run time it is an array of field names and indexes, such as ["attachments", 0]. Print it with JSON.stringify(issue.path).
  • The retry_at field of the API error is not exposed.
APIConnectionError has no status, code, traceId or requestId. The original error is in cause. Its messages are: The last attempt is the only one when maxRetries is 0. To tell a timeout from your own cancel, test signal.aborted on the signal you passed. When the request of attachments.upload to the upload URL fails, the helper throws an APIConnectionError with a message of its own. When that request is refused, it throws APIError with code upload_failed and the status of that request. Neither error carries the attachment id. An error message can contain up to 200 characters of a response body that is not JSON. Log code, status, traceId and requestId in preference to message.

Branch on code

The class tells the status. err.code tells the exact cause: branch on it. The codes of each operation are on its reference page, and the codes every operation shares are in Errors.

Codes the SDK raises itself

The SDK throws APIError with codes of its own, which the API never sends: One code is raised by both sides. attachments.upload refuses a file larger than 5,368,709,120 bytes before any request, with a plain APIError of status 413 and code attachment_exceeds_storage_limit. The same refusal by the API, on attachments.create, arrives as UnprocessableEntityError. The attachments helper describes these failures.

The two error classes of a 429

A 429 becomes QuotaExceededError when its code is one of five codes, and RateLimitError otherwise. Neither class extends the other: catch both. The last column describes a client with the default options. The codes of messages.send come from the sending limits of the line: see Sending limits. After one of them, read the counters of the line with client.lines.quota(id). rate_limit_exceeded is the request-rate limit, or on messages.send and messages.list the message-operations limit: see Rate limits.

Automatic retries

The SDK retries a failed attempt by itself, up to maxRetries times (3 by default; the pages of this site pass 2). Retries apply to every method, POST, PATCH and DELETE included. client.health() is the exception: it is not retried unless the call passes maxRetries. A RateLimitError whose delay is maxRetryDelayMs or less, 60 seconds by default, is retried too. On a line whose quota_overflow_behavior is drop or fail, a minute_limit_exceeded send is therefore retried 60 seconds later, and can then be accepted: see Refusals. Pass maxRetries: 0 to the call to receive the refusal immediately.

Delays

When the API states no delay, the wait before retry n (0, 1, 2) is 250 ms multiplied by 2 to the power n, plus a random 0 to 100 ms, capped at maxRetryDelayMs. With the defaults the three waits are 250 to 350 ms, 500 to 600 ms, and 1,000 to 1,100 ms. On a 429 that carries error.retry_after, the SDK waits exactly that number of seconds. When that delay is longer than maxRetryDelayMs, the SDK does not wait: it throws the RateLimitError immediately, with retryAfter set, and does not call onRetry. The SDK reads the delay from error.retry_after in the response body. It does not read the Retry-After header. The API sets both to the same value.

How long a call can last

timeoutMs applies to each attempt, not to the call. A call can last much longer than timeoutMs:
  • No response. With the defaults, a call to a server that never responds lasts at least 121.75 seconds: 4 attempts of 30 seconds and 3 waits.
  • Request-rate limits. Over the request-rate limit or the message-operations limit (see Rate limits), the API returns 429 rate_limit_exceeded with a retry_after of 60. With the defaults the SDK waits 60 seconds and retries, up to 3 times: the call lasts more than 180 seconds before it throws RateLimitError.
To fail fast, lower maxRetryDelayMs on the client, or pass maxRetries: 0 to a call. To stop a call, retries included, cancel it with a signal.

Idempotency keys

The auto mode

In "auto" mode, the default, the SDK adds an Idempotency-Key header, a random UUID (version 4), to every POST, PATCH, PUT and DELETE call. GET calls carry none. The key is generated once per method call and reused on the automatic retries of that call. Two separate method calls get two different keys.

Operations that honor the key

The API acts on the key for these eight methods only. On every other method the header is sent and ignored. On messages.send, messages.addReaction, messages.removeReaction and chats.shareContactCard, the key belongs to the line: the same key used on another line is a separate call, and returns no conflict. In attachments.upload, the API acts on the key for the request that registers the attachment, and for that request only. Each upload() call registers a new attachment, unless you pass an idempotencyKey that an earlier call used: the API then returns the attachment registered with that key while it is awaiting_upload and before its upload deadline, and the call throws ConflictError with code idempotency_terminal_state in any other state. What a repeated request returns, for how long, and what the key is scoped to differ between these operations: see Operations that honor the key.

The other writes

Automatic retries apply to every method. On a method outside Operations that honor the key, a retry after a lost response can run the operation twice. This concerns webhooks.create, webhooks.rotateSecret, webhooks.replay, trial.init, byod.pair, preOrders.create and every other method that writes. Pass maxRetries: 0 where a second execution is not acceptable. webhooks.create is one such call: two subscriptions for one URL deliver every event twice.
The signing secret of a subscription is returned by webhooks.create and by webhooks.rotateSecret only. When the response of the creation was lost, call webhooks.rotateSecret to get a secret. Three more cases need care with automatic retries:
  • A retry of a write that succeeded can throw instead of acting twice: messages.cancel throws ConflictError with too_late_to_cancel, lines.cancelVariantChange NotFoundError with no_pending_variant_change, lines.release NotFoundError, and preOrders.cancel BadRequestError with pre_order_already_confirmed. Read the resource before you report a failure.
  • lines.assign with as_test_line: true: when the conversion to a test line fails after the assignment, the API does not record the key, and an automatic retry of a 500 assigns another line. Pass { maxRetries: 0 } with as_test_line, and read lines.listMine before you call again.
  • trial.status: call it with { maxRetries: 0 }, on a client whose timeoutMs covers your network. The first read within 1 hour after pairing returns the trial key, and no later read returns it: when that response is lost, a retry does not return the key.

Your own retries

Retry a write only on an operation that honors Idempotency-Key, with the key of the first attempt. A new method call gets a new key, so pass the key yourself. Build it from a record of your own, so that every attempt to send the same message carries the same key, in any process:
A second messages.send with the same key, on the same line and with the same content, returns the message already created, in its current state. No second message is sent. The key stays valid as long as that message exists: see Idempotency. Three rules apply to your own retries:
  • Send retries one after the other, not in parallel, and keep the key at 255 characters or fewer.
  • Do not retry chats.shareContactCard with the same key: the retry throws ConflictError with code idempotency_conflict once the first request created the message, and sends the card when it did not. Pass maxRetries: 0 to that method and, after a failure, look for the message with messages.list before you call it again.
  • On lines.assign, lines.mintShared and macosSessions.rent, a repeat of the same request, sent while the first one is still running, returns 503 idempotency_in_flight. The SDK retries it with the same key and, with the defaults, throws ServiceUnavailableError after its three waits, 1.75 to 2.05 seconds in total. The API reserves a key for up to 60 seconds. Pass your own idempotencyKey to these methods, so that you can call again with the same key. The API remembers the key for 24 hours: after that, the same call with the same key takes or creates another line, or rents another session.

The manual mode

In "manual" mode the SDK sends a key only when the call passes one. Automatic retries stay on: a write without a key can execute twice. TypeScript does not require a key in this mode.

Cancellation

Pass an AbortSignal as signal to cancel a call. Cancellation stops the current attempt while it waits for the response headers, and any wait before a retry. Once the headers have arrived, the SDK reads the body to the end. When the cancellation takes effect, the call rejects with APIConnectionError, whose cause is the reason of the signal. The message of the error depends on when the signal was aborted: see Fields of an error. Do not read the message to tell your cancel from a timeout: test signal.aborted, as the sample does.
A signal passed to a list iterator also stops the iteration: see Stop an iteration.