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 fromerror.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
EveryAPIError carries these fields:
Three points need care:
- The type declarations give
retryAfterandissuesas optional. At run time they arenullorundefinedwhen they do not apply. Test them withtypeof err.retryAfter === 'number'andArray.isArray(err.issues). - The type declarations give
pathas astring. At run time it is an array of field names and indexes, such as["attachments", 0]. Print it withJSON.stringify(issue.path). - The
retry_atfield 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 throwsAPIError 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 tomaxRetries 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 retryn (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_exceededwith aretry_afterof 60. With the defaults the SDK waits 60 seconds and retries, up to 3 times: the call lasts more than 180 seconds before it throwsRateLimitError.
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 concernswebhooks.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.
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.cancelthrowsConflictErrorwithtoo_late_to_cancel,lines.cancelVariantChangeNotFoundErrorwithno_pending_variant_change,lines.releaseNotFoundError, andpreOrders.cancelBadRequestErrorwithpre_order_already_confirmed. Read the resource before you report a failure. lines.assignwithas_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 a500assigns another line. Pass{ maxRetries: 0 }withas_test_line, and readlines.listMinebefore you call again.trial.status: call it with{ maxRetries: 0 }, on a client whosetimeoutMscovers 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 honorsIdempotency-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:
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.shareContactCardwith the same key: the retry throwsConflictErrorwith codeidempotency_conflictonce the first request created the message, and sends the card when it did not. PassmaxRetries: 0to that method and, after a failure, look for the message withmessages.listbefore you call it again. - On
lines.assign,lines.mintSharedandmacosSessions.rent, a repeat of the same request, sent while the first one is still running, returns503 idempotency_in_flight. The SDK retries it with the same key and, with the defaults, throwsServiceUnavailableErrorafter its three waits, 1.75 to 2.05 seconds in total. The API reserves a key for up to 60 seconds. Pass your ownidempotencyKeyto 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 anAbortSignal 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.