Skip to content

Error Handling

Every client method returns a promise. When Jira answers with a non-2xx status, or the request never reaches it, the promise rejects with one of the library's own error types.

typescript
import { isNotFoundError } from 'jira.js';

try {
  const issue = await jira.issues.getIssue({ issueIdOrKey: 'TEST-1' });
} catch (error) {
  if (isNotFoundError(error)) return null;
  throw error;
}

The error types

ErrorWhenExtra fields
ApiErrorAny non-2xx response; base of the ones belowstatus, statusText, body
AuthError401 — credentials missing, expired or wrong
ScopeError401 where the token lacks the required scope
ForbiddenError403 — authenticated, not permitted
NotFoundError404
RateLimitError429retryAfterMs
ServerError5xx
NetworkErrorThe request never completed — DNS, TLS, socketcode
OAuthErrorThe token flow itself failed
ConfigErrorThe client was configured with something impossible
SchemaMismatchError2xx, but not the shape the endpoint promisesreport

body carries Jira's own error payload, which is usually { errorMessages, errors }:

typescript
import { isApiError } from 'jira.js';

try {
  await jira.issues.createIssue({ fields });
} catch (error) {
  if (isApiError(error)) {
    console.error(error.status, error.body);
  }
}

Use the predicates, not instanceof

Every error ships with a predicate — isApiError, isAuthError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isOAuthError, isConfigError, isSchemaMismatchError, isScopeError.

They read a branded symbol rather than walking the prototype chain, so they keep working when a bundler splits chunks, when minification renames classes, and when two copies of the package end up in one node_modules — all cases where instanceof silently returns false.

The predicates nest the way the types do: a NotFoundError satisfies isApiError too.

Rate limits

RateLimitError.retryAfterMs is Jira's Retry-After, already converted to milliseconds:

typescript
import { isRateLimitError } from 'jira.js';

try {
  await jira.issueSearch.searchForIssuesUsingJqlEnhancedSearchPost({ jql });
} catch (error) {
  if (isRateLimitError(error) && error.retryAfterMs) {
    await new Promise(resolve => setTimeout(resolve, error.retryAfterMs));
  }
}

Retrying transient failures

Retries are off by default — masking a failure hides real regressions. Opt in for the transport-level ones only:

typescript
const jira = createCloudClient({
  host,
  auth,
  retry: { maxAttempts: 3, initialDelayMs: 500, backoffFactor: 2 },
});

This covers network errors and 502/503/504. It never retries a 4xx — including 429, which carries its own Retry-After and deserves a considered wait rather than a blind one — nor any other 5xx.

OAuth 2.0 and 401s

Under OAuth 2.0 the client refreshes the access token before expiry and retries once on a 401. A second 401 propagates. If refreshing itself fails, you get OAuthError; isReauthorizationRequired(error) tells you the user has to grant consent again rather than the call being retryable. See the OAuth 2.0 guide.

Responses that arrive but do not fit

A 2xx whose body does not match the endpoint's schema is not an error by default — it is reported and handed back. See Response Validation.