Overview
The SDK raises errors through two related classes:
SdkError— thrown by SDK code itself (validation, configuration, deprecated paths, internal invariants). Always carries acode, astatus(HTTP-like), and an optionaloriginalError. Since #189originalErroris non-enumerable: it stays readable aserr.originalErrorfor 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 itsconfig) can't leak through generic serialization. Prefercode/status/messagefor anything you log.SdkError'sdescriptionis not redacted.AjaxErrorruns itsrequestInfothroughredactSensitiveParams;SdkErrorhas no equivalent step, because its description is expected to be written by the SDK rather than assembled from input. If you construct anSdkErroryourself, 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. AddsrequestInfo(method,requestId, request params) so you can correlate with portal-side logs, andisV3Envelope, saying whether the portal answered in therestApi:v3error shape ({ error: { code, message } }) rather than the flat v2 one — which is what the category rule keys on, and isundefinedfor an error that never came from a parsed REST body. Since v1.1.2 (#39),requestInfodoes not include the full request URL and credential-bearing fields insideparamsare redacted — the goal is to keep webhook secrets out oftoJSON()/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.
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.
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.
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.Everything not overridden answers 400: RestException::STATUS is 400 Bad Request and only the rows above change it.
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
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 membersisMore,hasMore,getTotal,getNextandfetchNextarerestApi:v2-only and none is deprecated. Under v3 the readers answerfalse/0(the field is absent) andgetNext()/fetchNext()throwJSSDK_CORE_METHOD_NOT_SUPPORT_IN_API_V3. For new paging code preferb24.actions.v{2,3}.callList.makeorfetchList.make, which work under both versions.
Filtering
Reference for building filter parameters in Bitrix24 REST API v2 (prefix operators) and v3 (array-of-triples), including date formatting and the order-stripping rule.
Discovering v3 methods
Use rest.documentation.openapi to fetch the portal's own machine-readable list of every available REST API v3 method — the source of truth the SDK relies on instead of a hardcoded allowlist. Especially useful for AI agents and codegen.