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
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
isUserError | HTTP Status | Meaning | Action |
|---|---|---|---|
true | 400, 401, 403, 404, 409, 422 | The request has an issue the user can fix | Show the message to the user |
false | 429 | Rate limit | Tell user to wait and retry |
false | 500, 502, 503, 504 | Server-side issue | Tell user to try again later |
false | 0 | Network error or timeout | Tell 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).
| Field | Error code | Triggered when |
|---|---|---|
| Card number | incomplete_number | Fewer digits than the detected card scheme requires |
| Card number | invalid_number | Fails the Luhn checksum |
| Expiry | incomplete_expiry | Fewer than 4 digits entered |
| Expiry | invalid_expiry_month | Month is not 1–12 |
| Expiry | invalid_expiry_year | The 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).
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
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
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
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
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
- Always wrap SDK calls in try/catch. Every async method can throw
APIError. - Use
isUserErrorto decide messaging.true→ show the specific error to the user.false→ show a generic retry message. - Handle
onTimeoutin polling. Payment may still be processing server-side after timeout. - Clean up on navigation. Call
bahn.cleanup()or individual unmount methods when the user navigates away.