v2.2.0

Error codes and handling

Reference for SdkError and AjaxError codes raised by the SDK, plus the Bitrix24 REST error codes that surface through them.

Overview

The SDK raises errors through two related classes:

  • SdkError — thrown by SDK code itself (validation, configuration, deprecated paths, internal invariants). Always carries a code, a status (HTTP-like), and an optional originalError. Since #189 originalError is non-enumerable: it stays readable as err.originalError for local debugging, but a spread { ...err }, Object.keys(err), JSON.stringify(err), or a Sentry-style capture skips it — so the raw transport error (which may carry a webhook secret in its config) can't leak through generic serialization. Prefer code / status / message for anything you log.
    SdkError's description is not redacted.AjaxError runs its requestInfo through redactSensitiveParams; SdkError has no equivalent step, because its description is expected to be written by the SDK rather than assembled from input. If you construct an SdkError yourself, do not interpolate request params, a filter, a URL or a token into it — a filter alone legitimately carries user data, such as the email or phone number being searched for, and error messages travel into logs and failure reports.
  • AjaxError extends SdkError — thrown when an HTTP call to Bitrix24 fails. Adds requestInfo (method, requestId, request params) so you can correlate with portal-side logs, and isV3Envelope, saying whether the portal answered in the restApi:v3 error shape ({ error: { code, message } }) rather than the flat v2 one — which is what the category rule keys on, and is undefined for an error that never came from a parsed REST body. Since v1.1.2 (#39), requestInfo does not include the full request URL and credential-bearing fields inside params are redacted — the goal is to keep webhook secrets out of toJSON() / toString() output.

Method-style results that don't throw — Call, CallList, Batch, BatchByChunk — surface portal-side failures through Result/AjaxResult: check .isSuccess and read .getErrorMessages(). FetchList, by contrast, does throw on failure (the generator can't complete partially).

That split is about REST errors, not about every failure. CallList and CallTail also raise SdkError for the handful of conditions that make the walk itself impossible — a filter they cannot extend, a cursor that stops advancing (JSSDK_ACTION_CURSOR_STALLED) or moves backwards (JSSDK_ACTION_CURSOR_WENT_BACKWARDS) and a maxPages that is not a positive integer (JSSDK_ACTION_INVALID_MAX_PAGES). Those reject the promise rather than resolving with a Result, so wrap the call in try/catch as well as checking isSuccess if you want to handle both. Two more conditions — a walk that hits its page ceiling (JSSDK_ACTION_MAX_PAGES_EXCEEDED) and one the caller aborted (JSSDK_ACTION_ABORTED) — apply to FetchList and FetchTail equally, and are the exception to the split above: the eager walkers resolve, handing back the rows they read with the error attached, because those rows are correct and merely incomplete. If you are upgrading inside the 2.x line and this is new to you, see Behaviour changes inside 2.x.

Which field failed: AjaxError.validation

restApi:v3 reports a validation failure with a validation array naming the field, and AjaxError carries it verbatim (#423):

readonly validation?: ReadonlyArray<{ field?: string, message?: string }>

getErrorMessages() folds those messages into one string for display. validation keeps them apart, with the field each belongs to — which the message alone does not carry, and which is what you need to mark the offending input rather than show a banner:

import { AjaxError } from '@bitrix24/b24jssdk'

declare function markFieldInvalid(field?: string, message?: string): void

const response = await $b24.actions.v3.call.make({
  method: 'note.document.get',
  params: {}
})

if (!response.isSuccess) {
  for (const error of response.getErrors()) {
    if (error instanceof AjaxError && error.validation) {
      for (const detail of error.validation) {
        // detail.field → 'id'
        // detail.message → 'Обязательное поле `id` не указано'
        //                  ("Required field `id` is not specified" — the portal
        //                   answers in the portal's own language)
        markFieldInvalid(detail.field, detail.message)
      }
    }
  }
}

The call above is the live example this was verified with: a restApi:v3 method called without its required argument answers 400 with

{"error":{"code":"BITRIX_REST_V3_EXCEPTION_VALIDATION_REQUESTVALIDATIONEXCEPTION",
  "message":"Ошибка при валидации объекта запроса",
  "validation":[{"message":"Обязательное поле `id` не указано","field":"id"}]}}

("Ошибка при валидации объекта запроса" — "Error validating the request object". Portal messages come back in the portal's language, so treat them as text to show a user, not as something to match on; match on code and field.)

A request that fails on several fields returns one entry per field, and message folds them all into one string separated by spaces.

Each entry is run through the same redactor as requestInfo.params, so a key named token or password inside an entry is masked — see Logging.

field and message are both optional inside an entry, because the portal's own shape says so — TypeDescriptionErrorV3 declares them optional and permits extra keys. Handle field being absent rather than asserting it.

Present only for restApi:v3, and only when the portal sent one — restApi:v2 has no equivalent, and a v3 error can fail for reasons that carry no field. It reaches you whether the code is soft (returned on the Result) or hard (thrown), since both paths now build the error the same way. toJSON() includes it when it is present, so it survives into a log or an error tracker — which is where the field name matters most, since message folds the validation messages in but not the field each came from.

import { SdkError, AjaxError } from '@bitrix24/b24jssdk'

try {
  // …SDK calls
}
catch (error) {
  if (error instanceof AjaxError) {
    // network / portal-side failure; error.code is the Bitrix24 code
    console.error(error.code, error.status, error.requestInfo?.method, error.requestInfo?.requestId)
  }
  else if (error instanceof SdkError) {
    // SDK-side issue; error.code starts with JSSDK_
    console.error(error.code, error.status)
  }
  else {
    throw error
  }
}

SdkError codes raised by the SDK

Codes are stable strings — match on them, don't parse messages.

CodeStatusWhere it's thrownWhat it means
JSSDK_CORE_B24_NOT_INIT500AbstractB24 gettersYou called a method on a B24 instance whose constructor never finished. For B24Frame, await initializeB24Frame(); for B24Hook, the URL was malformed.
JSSDK_CORE_B24_HTTP_V2_NOT_INIT500getHttpClient(ApiVersion.v2)The v2 HTTP client wasn't built (typically a configuration bug).
JSSDK_CORE_B24_HTTP_V3_NOT_INIT500getHttpClient(ApiVersion.v3)Same, for v3.
JSSDK_CORE_B24_API_WRONG500getHttpClientAn unknown ApiVersion was requested.
JSSDK_CORE_METHOD_NOT_SUPPORT_IN_API_V3500AjaxResult.getNext()getNext() is restApi:v2-only and cannot work against a v3 client — v3 sends no next offset, so it throws rather than returning false, which would read as "last page". CallV3.make() / BatchV3.make() no longer throw this — the SDK dropped its v3 method allowlist, so an unknown v3 method is reported by the server as METHODNOTFOUNDEXCEPTION (a soft error on the result).
JSSDK_CORE_B24_FETCH_LIST_METHOD_API_V2500FetchListV2.make() generatorAn underlying page request failed; the generator stops. Catch around for await.
JSSDK_CORE_B24_FETCH_LIST_METHOD_API_V3500FetchListV3.make() generatorSame, for v3.
JSSDK_CORE_B24_FETCH_TAIL_METHOD_API_V3500FetchTailV3.make() generatorAn underlying tail (keyset) page request failed; the generator stops. Catch around for await.
JSSDK_CORE_B24_FETCH_TAIL_DESC_REQUIRES_INITIAL_VALUE / JSSDK_CORE_B24_CALL_TAIL_DESC_REQUIRES_INITIAL_VALUE500FetchTailV3.make() / CallTailV3.make()order: 'DESC' was requested without initialValue; the server pages by field < value, so the default 0 returns nothing. Pass initialValue.
JSSDK_FILTER_V3_INVALID_FIELD / JSSDK_FILTER_V3_INVALID_OPERATOR / JSSDK_FILTER_V3_INVALID_IN / JSSDK_FILTER_V3_INVALID_BETWEEN / JSSDK_FILTER_V3_INVALID_NODE400FilterV3 builderClient-side validation while building a v3 filter: empty field name, operator outside the 8 allowed, in value not a non-empty array, between with an undefined/null operand, or a malformed node passed to build() (e.g. a forgotten spread).
JSSDK_BATCH_REF_V3_INVALID_PATH / JSSDK_BATCH_REF_V3_INVALID_REF_ARRAY400BatchRefV3 helperClient-side validation of a v3 batch $ref / $refArray marker: empty/non-string path, or a refArray path without a dot (alias.field).
JSSDK_AGGREGATE_V3_INVALID_FUNCTION400AggregateV3 actionThe select names a function outside sum/avg/min/max/count/countDistinct.
JSSDK_AGGREGATE_V3_INVALID_SELECT400AggregateV3 actionA select value that is neither a string[] nor a { field: alias } map.
JSSDK_AGGREGATE_V3_EMPTY_SELECT400AggregateV3 actionThe select names no aggregate column at all ({}, { count: [] }, { count: {} }). The portal answers such a request with a bare 500 that carries nothing to act on, so it is refused before the request goes out. The count is over the whole select: an empty list beside a non-empty one is accepted, because the portal accepts it.
JSSDK_BATCH_REF_V3_INVALID_PATH / JSSDK_BATCH_REF_V3_INVALID_REF_ARRAY400BatchRefV3 helperClient-side validation while building a v3 batch $ref/$refArray marker: empty/non-string path, or a refArray path without a dot (alias.field).
JSSDK_BATCH_TOO_LARGE400v2 and v3 HTTP clientsMore than 50 commands handed to a single batch. On restApi:v2 50 is the server's own limit; on restApi:v3 it is the SDK's — 51 commands are accepted by the portal, so the ceiling is a deliberate match to v2 rather than a protocol boundary. Use BatchByChunk.
JSSDK_BATCH_EMPTY400v2 HTTP clientEmpty calls.
JSSDK_BATCH_SUB_ERROR500batch processingWrapper for a sub-call failure inside a batch. Inspect the per-call result.
JSSDK_INTERACTION_BATCH_BUILD_STRATEGY_V3_EMPTY_COMMAND400v3 batch processingA single command in a v3 batch was empty/malformed.
JSSDK_INTERACTION_BATCH_BUILD_STRATEGY_V3_EMPTY_COMMANDS400v3 batch processingThe whole commands array was empty.
JSSDK_INTERACTION_BATCH_STRATEGY_V2_EMPTY_COMMANDS400v2 batch processingSame, for v2.
JSSDK_INTERACTION_BATCH_STRATEGY_V2_EMPTY_COMMAND_RESPONSE500v2 batch processingA command in the response had no body — usually portal-side.
JSSDK_INTERACTION_BATCH_EMPTY_PROCESSING_STRATEGY500batch processingInternal — strategy lookup failed.
JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY500actions.v3.callList / fetchListfilter was not an array. These actions append [cursorIdKey, '>', cursor] to it on every page, so only an array can be extended — a FilterV3 logic group is a valid v3 filter but must be wrapped: filter: [FilterV3.or(...)].
JSSDK_ACTION_V3_TAIL_FILTER_INVALID500actions.v3.callTail / fetchTailfilter was the restApi:v2 object dialect ({ '>id': 100 }). These actions forward filter untouched, so an array or a bare logic group is fine — only the v2 dialect is refused, and it is refused here rather than one round trip later, where the portal reports it in wording that never names the dialect.
JSSDK_ACTION_CURSOR_STALLED500callList / fetchList (v2 and v3), callTail / fetchTail (v3)The cursor came back equal to the one just sent — the server is answering with the same page, so the walk can never end. A short page raises this too: a row at or before the cursor cannot be in an answer that honoured a strictly-greater condition, however few rows came back, so the cursor is checked before a short page is called the end of the data. List walkers: the > page condition was dropped, almost always because the id field is spelled one way in the response and another in the request — check idKey against cursorIdKey (on restApi:v2 tasks.task.list that pair is idKey: 'id', cursorIdKey: 'ID'; the v3 method is lowercase both ways and needs no override). Tail walkers: check cursorField — it must be the field the server pages by, be present in select, and advance between pages. Unlike a soft REST error this rejects instead of resolving. fetchList / fetchTail have already yielded every page they read, the repeated one included, so a for await consumer that persisted them has to undo that.
JSSDK_ACTION_CURSOR_WENT_BACKWARDS500callList / fetchList (v2 and v3), callTail / fetchTail (v3)The cursor moved, but the wrong way — into a value the walk had already passed. A server that alternates between two pages produces exactly this, and the stall check above cannot see it: the value differs from the one just sent, it is simply one already used, so the walk would run for ever. Every walk here asks for rows strictly past the cursor — the list walkers append [cursorIdKey, '>', cursor], the tail walkers send cursor: { field, value, order } — so a cursor that does not advance in the walk's own direction means the page condition was not applied. Check the same things as for a stall: idKey against cursorIdKey for a list walk, cursorField for a tail walk, and with order: 'DESC' that initialValue is the newest value rather than the oldest. Values the SDK cannot order against each other are not reported here; they fall back to the equality check. Only two string shapes are judged, and both only at equal length: digits (a zero-padded id), and ISO-8601 datetimes stating the same zone with the same separator. Everything else — a cursor whose type changed, unpadded ids, mixed-case values a case-insensitive collation may order differently from JavaScript, and timestamps a DST transition reorders, whether because the offset changes across it or because none is stated at all — is let through, because a guard that stops a healthy walk would be worse than one that misses a sick one.
JSSDK_ACTION_MAX_PAGES_EXCEEDED500callList / fetchList (v2 and v3), callTail / fetchTail (v3)The walk read maxPages pages without reaching the end of the data. Either the read is genuinely larger than the ceiling — raise maxPages, which defaults to 10 000 — or the walk is not making progress in a way the earlier guards cannot see — a repeated cursor is caught by JSSDK_ACTION_CURSOR_STALLED and a cycling one by JSSDK_ACTION_CURSOR_WENT_BACKWARDS, so what remains for the ceiling is a walk whose cursor values the SDK cannot order against each other. The eager walkers (callList / callTail) return the rows they did read with this error attached — the rows are correct, merely incomplete, so check isSuccess rather than assuming a returned list is whole. fetchList / fetchTail throw instead, having already yielded every page they read.
JSSDK_ACTION_ABORTED400the same six walkersThe signal passed to the walk fired. Checked at the top of each iteration, so an already-aborted signal costs no request at all. Handled like the ceiling above: the eager walkers return what they read with this attached, the streaming ones throw.
JSSDK_ACTION_INVALID_MAX_PAGES400the same six walkersmaxPages was not a positive integer. 0 would mean "walk nothing", and a fractional ceiling would fire at a page number nobody wrote — both are refused rather than coerced.
JSSDK_INTERACTION_BATCH_ROW_FAIL500batch row parserA single batch row could not be parsed.
JSSDK_INVALID_PARAMS400HTTP transportThe shape of params was rejected before the request was sent.
JSSDK_PARAMS_TOO_LARGE413HTTP transportSerialized request body exceeded the size limit. Split the call.
JSSDK_CALL_ALL_ATTEMPTS_EXHAUSTED500HTTP transportDegenerate config only (maxRetries < 1, no attempt made). On normal retry exhaustion the underlying error's real code is surfaced instead.
JSSDK_HTTP_INVALID_IDEMPOTENCY_KEY500_prepareRequestConfig (v3 transport)idempotencyKey is not 1-255 characters of printable ASCII (0x21-0x7E — no spaces, no control characters). restApi:v3 only: the v2 endpoint ignores the header, so the v2 transport drops the key with a warning rather than validating it. Thrown before the request is sent; the message never echoes the key, which a caller is free to build out of business identifiers.
JSSDK_HTTP_REDIRECT_BLOCKED0HTTP transportA redirect answered a request whose effective maxRedirects is 0. By default that is one request — a restApi:v3 batch on a non-hook transport, which carries an access token a redirect to a subdomain would take along — but a caller who sets maxRedirects: 0 in httpOptions or on ajaxClient.defaults puts every status-0 answer here, since an opaque response cannot be told from a dropped connection. Raised where the refusal is otherwise invisible: on the fetch adapter the answer is an opaque response (status 0, empty body) that axios resolves, so without this the refused request would read as an empty success. Outside a browser the same refusal arrives as a plain 301. With the default configuration a hook transport never reaches this code — a webhook sends no token, so none of its requests carry maxRedirects: 0, and the same opaque answer surfaces as JSSDK_INTERACTION_BATCH_STRATEGY_V3_EMPTY_COMMAND_RESPONSE; set the option yourself and a hook raises it like any other client. Not retried: the portal will answer the next attempt the same way. Point the SDK at the final URL.
JSSDK_UNKNOWN_ERROR500variousFallback when no more specific code applies.
JSSDK_INTERNAL_ERROR500SdkError.fromExceptionDefault code when wrapping an unknown exception.
JSSDK_INTERNAL_AJAX_ERROR500AjaxError.fromExceptionDefault code when wrapping an unknown HTTP error.
JSSDK_CLIENT_SIDE_WARNING— / 500B24Hook (browser / worker) · initializeB24FrameTwo cases. As a logged warning (no status): a B24Hook is used in a browser-like runtime — the main thread or a Web/Shared/Service Worker — where a webhook's portal-wide secret does not belong, because the bundle is readable either way. As a rejected SdkError (status: 500): initializeB24Frame() ran outside the Bitrix24 iframe, so window.name carried no DOMAIN/APP_SID — open the app inside Bitrix24 (or paste the URL into the app settings). The rejection is prompt and a later call may retry.
JSSDK_BATCH_UNREAD_COMMAND_KEYbatch command parsing (AbstractInteractionBatch)A logged warning, never thrown. One or more batch commands carried a key the SDK does not read — it reads method, params, as, paralleland lost their arguments by it: the command had no params, or it named query, the portal's own restApi:v3 wire spelling, which the SDK writes for you. Emitted once per batch request — a batchByChunk walk warns once per chunk, since each chunk is its own request — naming every such key and the positions of the commands that carried them. A key beside a populated params — your own id, label, _meta — is left alone. See BatchV3 and BatchV2.

REST-side codes that come back as AjaxError

Bitrix24 returns these in the error field of an HTTP response. The SDK lifts them into AjaxError.code verbatim — match on the string.

The two protocols use different vocabularies, and a code from one never appears on the other. The table below is restApi:v2; restApi:v3 has its own, built a different way.

CodeHTTP statusMeaningTypical fix
expired_token401OAuth access token expired.The SDK refreshes the token and retries once on B24Frame / B24OAuth; B24Hook retries with the same credentials (its refresh is a no-op).
invalid_token401Token is malformed or revoked.Handled like expired_token (auto-refresh + one retry); if the new token is still rejected the error surfaces — re-auth (OAuth) or fix the webhook URL.
AUTHORIZE_ERROR403Caller is not allowed to perform this action.Wrong scope on the webhook/app, or the user lacks the access right (e.g. CRM permission).
WRONG_AUTH_TYPE403The token type doesn't match the endpoint (e.g. webhook used where OAuth is required).Use the correct authentication method for the endpoint.
QUERY_LIMIT_EXCEEDED503Per-second / per-method rate limit hit.Already handled by the built-in RateLimiter — increase restrictionParams.maxConcurrent only if you understand the cost.
OVERLOAD_LIMIT503Portal-wide load shedding.Back off, retry after a few seconds. The SDK's AdaptiveDelayer already does this.
ERROR_METHOD_NOT_FOUND400Method name is wrong, or the app doesn't have the scope to see it.Check spelling; check app.info's SCOPE against the method's required scope.
INVALID_REQUEST400Bad parameters (missing required field, wrong type).Inspect error.message — Bitrix24's description names the offending field.

This is not exhaustive — Bitrix24 publishes the full method-specific list at apidocs.bitrix24.com. Anything not listed here passes through as-is on AjaxError.code.

restApi:v3 codes

v3 answers in a different shape — { error: { code, message } } rather than the flat v2 pair — and with a different set of codes. AjaxError.isV3Envelope says which shape a given error came from.

The list is mechanical, not curated. Every v3 error is a class under Bitrix\Rest\V3\Exception, and the code is its fully-qualified name, uppercased, with \ replaced by _RestException::getRegistryCode() does exactly that and nothing else. So Bitrix\Rest\V3\Exception\InvalidSelectException is always BITRIX_REST_V3_EXCEPTION_INVALIDSELECTEXCEPTION, and a class in a sub-namespace carries it: Validation\RequestValidationException becomes …_VALIDATION_REQUESTVALIDATIONEXCEPTION. Knowing the rule is worth more than memorising the table — a portal newer than these docs will answer with codes derived the same way.
Code (after the BITRIX_REST_V3_EXCEPTION_ prefix)HTTP statusMeaningTypical fix
ACCESSDENIEDEXCEPTION401 or 403Read the status, not the code — see the warning below.401: the credential was not accepted, for any reason. 403: the method or its controller is disabled on this portal.
INSUFFICIENTSCOPEEXCEPTION403The credential is valid but lacks the scope this method needs.Widen the webhook's scopes, or the app's; rest.scope.list shows what the token holds. Note that a method being listed by rest.documentation.openapi does not imply permission — see Discovering v3 methods.
METHODNOTFOUNDEXCEPTION404No such v3 method on this portal.Check spelling, and check the module is installed — v3 method sets differ sharply between portals.
RATELIMITEXCEPTION429Rate limit.Handled by the built-in RateLimiter; back off rather than raising concurrency.
RESTUNAVAILABLEEXCEPTION / MARKETSUBSCRIPTIONREQUIREDEXCEPTION402REST, or this app, is not available on the portal's plan.Commercial matter, not a code change.
VALIDATION_REQUESTVALIDATIONEXCEPTION400A required request field is missing or malformed — and the case where a field exists but lacks the attribute the request needs: Filterable to appear in filter or in an aggregate select, Sortable to appear in order.The response carries a validation array naming the field — that is what AjaxError.validation exposes. Match on the code and that field, never on the message: it is localised. Measured on tasks.task.list and main.eventlog.list; see Discovering entity fields.
INVALIDFILTEREXCEPTION / UNKNOWNFILTEROPERATOREXCEPTION400The filter is not a shape v3 parses, or names an operator it does not have.v3 filters are positional triples or logic groups — see Filtering. The restApi:v2 object dialect ({ '>id': 100 }) is not one of them.
INVALIDSELECTEXCEPTION / INVALIDORDEREXCEPTION / INVALIDPAGINATIONEXCEPTION400The select, order or pagination block is malformed. On a batch, INVALIDSELECTEXCEPTION is also what a request item missing method or query produces.Inspect error.message; the batch case names the requirement outright.
UNKNOWNDTOPROPERTYEXCEPTION400A field name the entity does not have.Read the real names from <entity>.field.list. A field that does exist but lacks the attribute the use needs answers VALIDATION_REQUESTVALIDATIONEXCEPTION instead — see the row above.
UNKNOWNAGGREGATEFUNCTIONEXCEPTION400An aggregate function outside sum / avg / min / max / count / countDistinct.The SDK's AggregateV3 rejects these before sending.
ENTITYNOTFOUNDEXCEPTION / ENTITYALREADYEXISTSEXCEPTION400The row is not there, or already is.Ordinary application-level outcomes; branch on them.
WRONGHTTPREQUESTMETHODEXCEPTION400The method was called with an HTTP verb it does not publish.The SDK always POSTs; this appears when a raw client GETs a POST-only method.
INTERNAL_INTERNALEXCEPTION500The portal hit something it will not describe — the message is a fixed "something went wrong".Nothing in the response to act on. Worth reporting with the request that produced it.

Everything not overridden answers 400: RestException::STATUS is 400 Bad Request and only the rows above change it.

On v3, one code covers every authentication failure.ACCESSDENIEDEXCEPTION with a 401 is what a portal answers to a wrong credential, a missing credential, and a perfectly good credential sent over plain HTTP — measured on one portal, three causes, byte-identical responses. The server's own explanation is discarded on the way out: RestApiServer::getRequestAccess() folds any checkAuth() failure into this one exception and keeps only the status. The same request on restApi:v2 answers INVALID_CREDENTIALS or INVALID_REQUEST / Https required. and tells you which.So ACCESSDENIEDEXCEPTION on v3 means "the request was not authorised" and nothing finer. Before reaching for scopes, check the boring things the code will not mention — the URL scheme, and whether the credential reached the request at all.The SDK delivers it as a soft error at both statuses — it is in the built-in soft list, which is consulted before any status is — so it arrives on the Result rather than thrown, and a 401 here does not trigger a token refresh (that path additionally requires expired_token or invalid_token). The status changes what you should conclude, not how the error reaches you.

Handling patterns

Distinguish SDK bugs from portal-side failures

AjaxError extends SdkError, so order the instanceof checks specifically-first:

// @check-ignore: top-level try/catch with return, not valid at module scope

import { AjaxError, SdkError } from '@bitrix24/b24jssdk'

try {
  await $b24.actions.v2.call.make({ method: 'crm.deal.get', params: { id: 1 } })
}
catch (error) {
  if (error instanceof AjaxError) {
    // Portal-side: error.code is the Bitrix24 code (e.g. expired_token)
    if (error.code === 'expired_token') return refreshAndRetry()
    if (error.code === 'AUTHORIZE_ERROR') return showPermissionDenied()
    throw error
  }
  if (error instanceof SdkError) {
    // SDK-side: error.code starts with JSSDK_
    throw error // these are programmer errors, surface them
  }
  throw error
}

// @check-ignore: top-level try/catch with return, not valid at module scope

import { AjaxError, SdkError } from '@bitrix24/b24jssdk'

try {
  await $b24.actions.v3.call.make({ method: 'tasks.task.get', params: { id: 1 } })
}
catch (error) {
  if (error instanceof AjaxError) {
    // Portal-side: error.code is the Bitrix24 code (e.g. expired_token)
    if (error.code === 'expired_token') return refreshAndRetry()
    if (error.code === 'AUTHORIZE_ERROR') return showPermissionDenied()
    throw error
  }
  if (error instanceof SdkError) {
    // SDK-side: error.code starts with JSSDK_
    throw error // these are programmer errors, surface them
  }
  throw error
}

Use isSuccess for non-throwing methods

Call, CallList, Batch, BatchByChunk return a Result / AjaxResult instead of throwing for a REST-side failure. CallList and CallTail still reject on the walk-level guards listed above (JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY, JSSDK_ACTION_V3_TAIL_FILTER_INVALID, JSSDK_ACTION_CURSOR_STALLED, JSSDK_ACTION_CURSOR_WENT_BACKWARDS), so add a try/catch if those matter to you:

// @check-ignore: top-level return in callList error-handling illustration

const response = await $b24.actions.v2.callList.make({ /* … */ })
if (!response.isSuccess) {
  console.error(response.getErrorMessages().join('; '))
  return
}
const items = response.getData()

// @check-ignore: top-level return in callList error-handling illustration

const response = await $b24.actions.v3.callList.make({ /* … */ })
if (!response.isSuccess) {
  console.error(response.getErrorMessages().join('; '))
  return
}
const items = response.getData()

For batches with isHaltOnError: false, isSuccess flips false on any sub-call failure but getData() still contains the successful entries. Iterate and check per-row .isSuccess if you need to know which calls passed.

Catch around for await for FetchList

// @check-ignore: top-level for-await in fetchList error-handling illustration

try {
  for await (const chunk of $b24.actions.v2.fetchList.make({ /* … */ })) {
    await persist(chunk)
  }
}
catch (error) {
  if (
    error instanceof SdkError
    && error.code === 'JSSDK_CORE_B24_FETCH_LIST_METHOD_API_V2'
  ) {
    // a page request failed; persisted chunks before this point are still good
    return resumeFromLastSavedId()
  }
  if (
    error instanceof SdkError
    && (error.code === 'JSSDK_ACTION_CURSOR_STALLED' || error.code === 'JSSDK_ACTION_CURSOR_WENT_BACKWARDS')
  ) {
    // not the same situation: the walk was repeating pages, so the chunks
    // already persisted contain duplicates and have to be rolled back
    return discardAndReportStalledCursor()
  }
  throw error
}

// @check-ignore: top-level for-await in fetchList error-handling illustration

try {
  for await (const chunk of $b24.actions.v3.fetchList.make({ /* … */ })) {
    await persist(chunk)
  }
}
catch (error) {
  if (
    error instanceof SdkError
    && error.code === 'JSSDK_CORE_B24_FETCH_LIST_METHOD_API_V3'
  ) {
    // a page request failed; persisted chunks before this point are still good
    return resumeFromLastSavedId()
  }
  if (
    error instanceof SdkError
    && (error.code === 'JSSDK_ACTION_CURSOR_STALLED' || error.code === 'JSSDK_ACTION_CURSOR_WENT_BACKWARDS')
  ) {
    // not the same situation: the walk was repeating pages, so the chunks
    // already persisted contain duplicates and have to be rolled back
    return discardAndReportStalledCursor()
  }
  throw error
}

Decide what to retry

Error conditionSafe to retry?
QUERY_LIMIT_EXCEEDED, OVERLOAD_LIMITYes — the built-in limiter already does.
expired_token, invalid_token (401)Yes — the SDK auto-refreshes the token and retries once on every entry point.
AUTHORIZE_ERROR, WRONG_AUTH_TYPE (403)No — needs human intervention (re-auth, fix scopes).
ERROR_METHOD_NOT_FOUND, INVALID_REQUESTNo — request is malformed; retrying gives the same error.
Network / 5xx without a specific codeYes — the limiter retries up to maxRetries; on final failure the underlying error (its real code) is surfaced. Bump restrictionParams.maxRetries if you need more attempts.

See also

  • Choosing the right method — picks the right primitive before you have to read this page.
  • Restrictions System — rate limit, operating-time and adaptive delay configuration.
  • AjaxResult — full payload surface (isSuccess, getData, getErrorMessages). The paging members isMore, hasMore, getTotal, getNext and fetchNext are restApi:v2-only and none is deprecated. Under v3 the readers answer false / 0 (the field is absent) and getNext() / fetchNext() throw JSSDK_CORE_METHOD_NOT_SUPPORT_IN_API_V3. For new paging code prefer b24.actions.v{2,3}.callList.make or fetchList.make, which work under both versions.