Skip to content

Error Handling ​

All SDK errors are thrown as APIError instances. Understanding the error structure helps you display appropriate messages to your users.

For the wire-level codes the API itself returns (code, type, decline_code), see Error Codes.

APIError ​

typescript
class APIError extends Error {
  name: "APIError";
  status: number;           // HTTP status code (0 for network errors)
  message: string;          // Human-readable message
  isUserError: boolean;     // true if the error is user-correctable (4xx)
  code?: string;           // Optional machine-readable error code
}

The SDK script only exposes Bahn as a global — APIError itself isn't, so identify SDK errors by err.name === "APIError" rather than instanceof.

Error Classification ​

isUserErrorHTTP StatusMeaningAction
true400, 401, 403, 404, 409, 422The request has an issue the user can fixShow the message to the user
false429Rate limitTell user to wait and retry
false500, 502, 503, 504Server-side issueTell user to try again later
false0Network error or timeoutTell user to check their connection

Status Code → User Message Mapping ​

The SDK maps HTTP status codes to user-friendly messages internally and sets them as err.message on the thrown APIError:

400 → "Invalid request. Please check your information and try again."
401 → "Authentication failed. Please refresh the page and try again."
403 → "Access denied. Please contact support if this persists."
404 → "Resource not found. Please try again."
409 → "This request has already been processed. Please try again."
422 → "Invalid payment information. Please check your card details."
429 → "Too many requests. Please wait a moment and try again."
500 → "Server error. Please try again in a moment."
502/503/504 → "Service temporarily unavailable. Please try again later."

Any other status falls back to "{context} failed. Please try again."


Card Validation Error Codes ​

The card iframes validate input locally and surface an error code per field. Override the default messages via the errorMsg option on mountCard (see Card Payments).

FieldError codeTriggered when
Card numberincomplete_numberFewer digits than the detected card scheme requires
Card numberinvalid_numberFails the Luhn checksum
Expiryincomplete_expiryFewer than 4 digits entered
Expiryinvalid_expiry_monthMonth is not 1–12
Expiryinvalid_expiry_yearThe card has expired
CVC(single message)Length doesn't match the scheme (3 digits, or 4 for Amex)

For card number and expiry, errorMsg is a Record<string, string> keyed by the codes above. For CVC, errorMsg is a single plain string (there is one CVC error state).

typescript
bahn.mountCard("#card-container", {
  fields: {
    number: {
      errorMsg: {
        incomplete_number: "Please enter your full card number",
        invalid_number: "That card number isn't valid",
      },
    },
    expiry: {
      errorMsg: {
        incomplete_expiry: "Enter the expiry date",
        invalid_expiry_month: "Month must be 01–12",
        invalid_expiry_year: "This card has expired",
      },
    },
    cvc: {errorMsg: "Enter the security code on your card"},
  },
});

3DS Error Codes ​

3DS challenge failures surface a 3DS error code string on error.code. See 3DS Authentication → 3DS Error Codes for the full list and a switch example.


Common Error Scenarios ​

Card component not mounted ​

typescript
try {
  await bahn.confirmPayment({client_secret});
} catch (err) {
  if (err?.name === "APIError" && err.status === 0) {
    // "Card component not mounted. Call mountCard() before confirmPayment()."
    bahn.mountCard("#card-container");
  }
}

Invalid card details ​

typescript
try {
  await bahn.confirmPayment({client_secret});
} catch (err) {
  if (err?.name === "APIError" && err.isUserError) {
    // Show the message to the user — it's something they can fix
    showErrorToast(err.message);
  } else if (err?.name === "APIError") {
    // Server or network error — show a generic message
    showErrorToast("Something went wrong. Please try again.");
  }
}

3DS failure ​

typescript
bahn.mount3DS(sessionId, {
  onSuccess: () => {
    // Continue payment flow
  },
  onFailure: (error) => {
    // error.code — one of the 3DS error codes, e.g. "challenge-failed"
    // error.message — human-readable description
    console.error(`3DS failed: ${error?.code} — ${error?.message}`);
    showBlockingError("3D Secure authentication failed. Please try a different card.");
  },
});

Payment intent polling timeout ​

typescript
bahn.pollPaymentIntentStatus(clientSecret, {
  onSuccess: (pi) => redirectToSuccess(),
  onFailure: (pi) => redirectToFailure(),
  onTimeout: () => {
    // Polling exhausted — payment may still be processing server-side
    // Consider showing a "We'll notify you" message
    showDelayedResultMessage("Your payment is being processed. We'll notify you when it completes.");
  },
  onError: (err) => {
    console.error("Poll error:", err);
  },
});

Best Practices ​

  1. Always wrap SDK calls in try/catch. Every async method can throw APIError.
  2. Use isUserError to decide messaging. true → show the specific error to the user. false → show a generic retry message.
  3. Handle onTimeout in polling. Payment may still be processing server-side after timeout.
  4. Clean up on navigation. Call bahn.cleanup() or individual unmount methods when the user navigates away.

Developers