# Error handling

Understand error types, handle payment failures, and implement proper error recovery.

## Overview

Drop-in reports almost all errors through your `onError` callback. The only exceptions are two configuration checks that throw when you create Drop-in: empty `sessionId` or empty `hmacKey`. Wrap `CheckoutDropIn(config:)` in `do/catch` to handle those.

Customer cancellation of the Apple Pay sheet (`SDK0615`) isn't delivered through `onError`. It's delivered through `methodConfig.global.onCancel(.applePay, error)` when that callback is registered. Other Apple Pay failures (e.g., `SDK0602`, `SDK0616`, `SDK0617`) still surface through `onError`.

**Aeropay:** Shopper closing the Pay by Bank popup fires `methodConfig.global.onCancel(.aeropay, nil)`, not `onError`.

Failures during `await dropIn.create()`, rendering, and payment processing don't throw. They arrive in `onError` instead. Plan your integration around that callback for runtime recovery and user messaging.

If multiple payment methods fail during `create()`, `onError` may be called more than once when `create()` completes (errors are queued in `pendingCreateErrors` and delivered once per failure).

Handle `onError` by inspecting `error.errorCode` (and `error.errorMessage` when the message adds useful context).

## Error callback

Register `onError` on `CheckoutDropInConfig` before calling `create()`. Use it for every error that happens after you have constructed Drop-in, including render and eligibility failures during `create()` and payment failures during checkout.

Reserve `do/catch` for initialisation only. It catches the two missing-session exceptions above. Don't expect payment or render errors to bubble up as Swift throws — including Aeropay failures such as `SDK0114`, `SDK0116`, `SDK1300`, `SDK0113`, and `SDK0115`, which arrive in `onError` after `create()` completes.

```swift
import PXPCheckoutSDK

do {
    let config = CheckoutDropInConfig(
        // ... other config
        onError: { paymentMethod, error in
            print("Payment failed: \(error.errorMessage)")
            print("Error code: \(error.errorCode)")
            
            // Handle the error (including create() failures)
            showErrorMessage(error.errorMessage)
        }
    )
    
    let dropIn = try CheckoutDropIn(config: config)
    await dropIn.create() // create() failures arrive in onError, not as throws
} catch {
    // Initialisation failed: sessionId or hmacKey missing
    print("Failed to initialise Drop-in: \(error.localizedDescription)")
    showErrorMessage("Failed to load payment form. Please contact support.")
}
```

### Error object structure

Each error passed to `onError` is a `BaseSdkException`. Inspect these properties:

| Property | Description |
|  --- | --- |
| `errorMessage`String | A human-readable description of what went wrong. |
| `errorCode`String | An SDK code in `SDK####` format (e.g., `SDK1114` for authentication failure or `SDK0500` for a network error). Some authorisation failures return a provider-specific code instead of a generic SDK code. Use `errorCode` to branch your handling logic. |


`onError` receives `(paymentMethod: DropInPaymentMethod?, error: BaseSdkException)`. Use the `paymentMethod` argument (not a property on `error`). It's `nil` when the failure isn't tied to a method (e.g., `SDK1100` during `create()`). Raw values include `Card`, `Paypal`, `ApplePay`, and `Aeropay`.

## Error categories

Errors are grouped below by source and type.

### Configuration errors

Errors that occur during SDK initialisation or Drop-in component rendering.

#### Initialisation errors (throws)

These errors throw from `CheckoutDropIn(config:)` when session credentials are missing:

| Error code | Description | Solution |
|  --- | --- | --- |
| `SDK0103` | `session.sessionId` is empty. | Provide a valid `SessionData` object from your backend. |
| `SDK0104` | `session.hmacKey` is empty. | Provide a valid HMAC key in `SessionData`. |


Other `SDK01xx` validation errors (e.g., missing funding types or intents) surface during `create()` through `onError`.

#### Drop-in render and update errors

These failures are delivered through `onError`, typically during `await dropIn.create()` or when Drop-in updates session state. `paymentMethod` is set when the failure is tied to a specific method. Branch on `error.errorCode`. If several methods fail to render, Drop-in may invoke `onError` once per failure when `create()` finishes.

| Error code | Description | Solution |
|  --- | --- | --- |
| `SDK1100` | Failed to retrieve Checkout Drop-In site configuration. | Check portal setup and network connectivity; retry `create()`. |
| `SDK1101` | Failed to render the card component. | Check session card configuration and funding types. |
| `SDK1102` | Failed to render the PayPal button. | Verify PayPal is enabled in the session. |
| `SDK1104` | Failed to render the Apple Pay button. | Check Apple Pay session and device configuration. |
| `SDK1125` | Failed to render the Aeropay component. Related setup codes such as `SDK0113` (credentials) or `SDK0115` (missing intent) can also appear when Aeropay can't load. | Check `payByBanks.aeropay` credentials and `intent.aeropay`. See [Aeropay](/guides/checkout/drop-in/ios/aeropay). |
| `SDK1105` | Failed to render saved cards. | Check token vault and shopper configuration. |
| `SDK1106` | Failed to render billing address fields. | Review billing address component configuration. |
| `SDK1107` | Failed to render the card brand selector. | Check accepted card networks in session or config. |
| `SDK1108` | Failed to render the card submit button. | Check card component wiring and session. |
| `SDK1109` | Failed to render saved-card submit. | Check card-on-file configuration. |
| `SDK1110` | Failed to render card consent UI. | Check site `storeCardConsent` and shopper setup. |
| `SDK1111` | Failed to render PayPal consent UI. | Check PayPal consent configuration. |
| `SDK1112` | Failed to update billing address on the session. | Retry or create a new session. |
| `SDK1113` | Failed to update allow-transaction state (Apple Pay). | Retry or create a new session. |
| `SDK1120` | PayPal only supports `entryType: .ecom`. | Change `entryType` to `.ecom`. |
| `SDK1121` | Component configuration problem during load. | Contact support with logs. |
| `SDK0204` | No payment methods available for the session. | Enable at least one funding type in the session response. |


Example handling:

```swift
onError: { paymentMethod, error in
    let isRenderOrSetupFailure =
        error.errorCode.hasPrefix("SDK110") || // SDK1100–SDK1109
        ["SDK1110", "SDK1111", "SDK1112", "SDK1113",
         "SDK1120", "SDK1121", "SDK1125", "SDK0204"].contains(error.errorCode) ||
        (paymentMethod == .aeropay &&
         ["SDK0113", "SDK0114", "SDK0115", "SDK0116", "SDK1300"].contains(error.errorCode))

    if isRenderOrSetupFailure {
        print("Rendering or setup error: \(error.errorMessage)")

        logger.error("Rendering error", [
            "error_code": error.errorCode,
            "message": error.errorMessage,
            "payment_method": paymentMethod?.rawValue ?? "none"
        ])

        Task { @MainActor in
            showErrorMessage(
                "We're having trouble loading payment options. " +
                "Please try again or contact support."
            )
        }
    }
}
```

### Payment errors

These errors occur when checkout reaches payment processing but the transaction does not complete. Handle them with `error.errorCode` (and `error.errorMessage` for decline details).

| Error code | Description | User action |
|  --- | --- | --- |
| `SDK1116` | Card payment failed. | See [card-specific patterns](#card-specific-error-detection-patterns). |
| `SDK1117` | PayPal transaction failed. | Try again or use a different method. |
| `SDK1119` | Apple Pay transaction failed. | Try again or use a different method. |
| `SDK1126` | Aeropay payment failed (Drop-in generic fallback when no API `errorCode` is present on a failed Unity submit). | Try again or use a different method. See [Aeropay](/guides/checkout/drop-in/ios/aeropay). Provider/gateway codes from failed submits may appear as `error.errorCode` via `CheckoutDropInFailedSubmitResultSdkException`, not necessarily `SDK1126`. |
| Provider-specific code | Authorisation returned a failed result with a gateway or issuer code (card, PayPal, Apple Pay, Aeropay). Log `error.errorCode` and `error.errorMessage`. Offer a retry or another payment method. |


#### Card-specific error detection patterns

Issuers and gateways format card decline messages differently. When you receive `SDK1116`, inspect `error.errorMessage` to choose an appropriate response:

| Scenario | Detection approach | User action |
|  --- | --- | --- |
| Card declined | `error.errorMessage` contains `"declined"` | Try a different card or contact the bank. |
| Insufficient funds | `error.errorMessage` contains `"insufficient funds"` | Use a different payment method. |
| Expired card | `error.errorMessage` contains `"expired"` | Use a different card. |
| Invalid CVV | `error.errorMessage` contains `"CVV"` or `"security code"` | Check the security code and retry. |
| Invalid card number | `error.errorMessage` contains `"card number"` or `"invalid number"` | Check the card number and retry. |
| Invalid expiry | `error.errorMessage` contains `"expiry"` or `"expiration"` | Check the expiry date and retry. |


Example handling:

```swift
onError: { paymentMethod, error in
    // Card-specific errors (message-based detection)
    if error.errorCode == "SDK1116" {
        // Card payment failed - check message for specifics
        let userMessage: String
        if error.errorMessage.localizedCaseInsensitiveContains("declined") {
            userMessage = "Your card was declined. Please try a different card or " +
                         "contact your bank for more information."
            Task { @MainActor in
                showErrorMessage(userMessage)
                offerAlternativePaymentMethods()
            }
        } else if error.errorMessage.localizedCaseInsensitiveContains("insufficient funds") {
            userMessage = "Insufficient funds. Please use a different payment method."
            Task { @MainActor in
                showErrorMessage(userMessage)
                offerAlternativePaymentMethods()
            }
        } else if error.errorMessage.localizedCaseInsensitiveContains("expired") {
            userMessage = "This card has expired. Please use a different card."
            Task { @MainActor in
                showErrorMessage(userMessage)
            }
        } else if error.errorMessage.localizedCaseInsensitiveContains("cvv") ||
                  error.errorMessage.localizedCaseInsensitiveContains("security code") {
            userMessage = "Invalid security code. Please check the CVV on the back of " +
                         "your card and try again."
            Task { @MainActor in
                showErrorMessage(userMessage)
                // Keep same payment method selected for retry
            }
        } else {
            userMessage = "Card payment failed. Please check your details and try again."
            Task { @MainActor in
                showErrorMessage(userMessage)
            }
        }
    }
    
    // Wallet payment errors
    else if error.errorCode == "SDK1117" {
        Task { @MainActor in
            showErrorMessage(
                "PayPal payment failed. Please try again or use a different payment method."
            )
            offerAlternativePaymentMethods()
        }
    } else if error.errorCode == "SDK1119" {
        Task { @MainActor in
            showErrorMessage(
                "Apple Pay payment failed. Please try again or use a different payment method."
            )
            offerAlternativePaymentMethods()
        }
    } else if paymentMethod == .aeropay {
        Task { @MainActor in
            let userMessage: String
            switch error.errorCode {
            case "SDK0114":
                userMessage = "Pay by Bank is only available for e-commerce checkout."
            case "SDK0116":
                userMessage = "Pay by Bank only supports USD."
            case "SDK0113", "SDK0115", "SDK1125":
                userMessage = "Pay by Bank is unavailable. Please choose another payment method."
            case "SDK1300":
                // Setup/eligibility: invalid non-empty onGetShopper data (not a payment decline)
                userMessage = "Pay by Bank could not start. Check shopper name, email, and US phone (+1 + 10 digits), or set methodConfig.aeropay.userId."
            case "SDK1126":
                userMessage = "Pay by Bank payment failed. Please try again or use a different payment method."
            case "SDK1303":
                // Mid-flow bank list / Aerosync payload (not create-time unavailability)
                userMessage = "Pay by Bank is not ready. Complete bank verification or choose another method."
            default:
                // Failed Unity submit may surface provider API error codes, not always SDK1126
                userMessage = error.errorMessage
            }
            showErrorMessage(userMessage)
            offerAlternativePaymentMethods()
        }
    }
}
```

### Authentication errors

These errors relate to 3D Secure (3DS) authentication during card payments. Related codes from the authentication flow may appear in `error.errorCode` or `error.errorMessage`.

| Error code | Description | User action |
|  --- | --- | --- |
| `SDK1114` | 3DS authentication failed. | Try again or use a different card. |
| `SDK0502` | The pre-initiate authentication step failed. | Try again or use a different card. |
| `SDK0503` | The issuer or gateway rejected authentication. | Try again or use a different card. |
| `SDK0505` | Authentication didn't complete successfully. | Try again or use a different card. |
| `SDK0510` | The SDK couldn't retrieve the authentication result. | Check the connection and try again. |
| `SDK0511` | The 3DS challenge page failed to load. | Check the connection and try again. |


When no specific code is present, you can still detect common outcomes from `error.errorMessage`. Treat the error as a timeout when the message contains `"timeout"`. Treat it as a customer cancellation when the message contains `"cancel"`.

Example handling:

```swift
onError: { paymentMethod, error in
    if error.errorCode == "SDK1114" || 
       error.errorCode == "SDK0502" ||
       error.errorCode == "SDK0503" ||
       error.errorCode == "SDK0505" ||
       error.errorCode == "SDK0510" ||
       error.errorCode == "SDK0511" ||
       error.errorMessage.localizedCaseInsensitiveContains("authentication") {
        if error.errorMessage.localizedCaseInsensitiveContains("timeout") {
            Task { @MainActor in
                showErrorMessage(
                    "3D Secure authentication timed out. Please check your " +
                    "internet connection and try again."
                )
                
                // Offer retry
                showRetryButton()
            }
        } else if error.errorMessage.localizedCaseInsensitiveContains("cancel") {
            Task { @MainActor in
                showErrorMessage(
                    "Authentication was cancelled. Please try again to complete " +
                    "your payment."
                )
                
                // Don't show error as prominently - user intentionally cancelled
                showInfoMessage("You can retry your payment when ready.")
            }
        } else {
            Task { @MainActor in
                showErrorMessage(
                    "3D Secure authentication failed. Please try again or use a " +
                    "different card."
                )
            }
            
            // Track authentication failures (pseudo-code - use your analytics provider)
            analytics.track("3ds_authentication_failed", properties: [
                "timestamp": Date().timeIntervalSince1970
            ])
        }
    }
}
```

### Authorisation errors

Errors that occur during payment authorisation.

| Error code | Description | Solution |
|  --- | --- | --- |
| `SDK1115` | Card authorisation evaluation failed in Drop-in (`CheckoutDropInAuthorisationFailedSdkException`). Not used for Aeropay. | Check the card transaction details and retry. For Aeropay Unity failures use API `errorCode`, `SDK1126`, or flow codes (`SDK1300`–`SDK1325`), not `SDK1115`. |


`SDK1115` is Drop-in **card** authorisation failure. Aeropay missing intent is **`SDK0115`** (configuration during create), not `SDK1115`. Don't branch on `SDK1115` for Aeropay setup.

### Aeropay flow errors

Aeropay setup and create-time codes (e.g., `SDK0113`–`SDK0116`, `SDK1125`, `SDK1300`) appear in `onError` as described above. Popup-stage failures can also surface through `onError` with codes in the `SDK1301`–`SDK1325` range (for example `SDK1301` OTP failure, `SDK1303` bank-list / Aerosync payload, `SDK1307` user not active, `SDK1308` get user failed, `SDK1319` aggregator credentials). Don't treat `SDK1311` as the primary “Pay by Bank unavailable at create” code.

For the full list and recommended handling, see [Aeropay — Error codes](/guides/checkout/drop-in/ios/aeropay#error-codes).

### Apple Pay errors

Errors specific to Apple Pay configuration, availability, and processing.

Customer cancellation (`SDK0615`) is delivered through `methodConfig.global.onCancel(.applePay, error)`, not `onError`. Register `onCancel` if you need to react to sheet dismissal. Other Apple Pay failures (e.g., `SDK0602`, `SDK0616`, `SDK0617`) still surface through `onError`. Aeropay popup close uses the same `onCancel` path with `.aeropay` and a `nil` payload.

| Error code | Description | Solution |
|  --- | --- | --- |
| `SDK0602` | Apple Pay isn't available on this device or iOS version. | Use another payment method; test on a supported device with Apple Pay set up. |
| `SDK0615` | Customer cancelled the Apple Pay sheet. | No action required — allow retry if needed. |
| `SDK0616` | Apple Pay merchant validation failed. | Verify Apple Pay merchant configuration in the Unity Portal. |
| `SDK0617` | Processing failed during Apple Pay payment. | Check logs and retry. |


### System errors

These errors reflect network connectivity, session state, or platform availability rather than a declined payment. They can appear for any payment method, or before a method is selected (`paymentMethod` may be `nil`).

Use `error.errorCode` where a code is listed. For message-based rows, inspect `error.errorMessage`:

| Scenario | How to detect | User action |
|  --- | --- | --- |
| Network error | `error.errorCode == "SDK0500"` | Check the connection and retry. |
| Session expired | `error.errorMessage` contains `"session"` or `"expired"` | Create a new session and reinitialise Drop-in. |
| Request timeout | `error.errorMessage` contains `"timeout"` | Check the connection and retry. |
| Service unavailable | `error.errorMessage` contains `"unavailable"` or `"service"` | Try again later. |
| Configuration error | `error.errorCode` starts with `"SDK01"` or `"SDK02"` | Review your session and config. Contact support if the issue persists. |


Example handling:

```swift
onError: { paymentMethod, error in
    // Network and session errors
    if error.errorCode == "SDK0500" {
        Task { @MainActor in
            showErrorMessage(
                "Network connection issue. Please check your internet connection " +
                "and try again."
            )
            showRetryButton()
        }
    } else if error.errorMessage.localizedCaseInsensitiveContains("session") ||
              error.errorMessage.localizedCaseInsensitiveContains("expired") {
        Task { @MainActor in
            showErrorMessage(
                "Your payment session has expired. Please reload checkout and " +
                "try again."
            )
            showRefreshButton()
        }
    } else if error.errorMessage.localizedCaseInsensitiveContains("timeout") {
        Task { @MainActor in
            showErrorMessage(
                "Request timed out. Please check your connection and try again."
            )
            showRetryButton()
        }
    } else if error.errorMessage.localizedCaseInsensitiveContains("unavailable") ||
              error.errorMessage.localizedCaseInsensitiveContains("service") {
        Task { @MainActor in
            showErrorMessage(
                "Payment service is temporarily unavailable. Please try again in " +
                "a few minutes."
            )
        }
    } else if error.errorCode.hasPrefix("SDK01") || error.errorCode.hasPrefix("SDK02") {
        Task { @MainActor in
            showErrorMessage(
                "Payment configuration error. Please contact support for assistance."
            )
        }
        
        // Log critical error
        logger.critical("Configuration error", [
            "error_code": error.errorCode,
            "message": error.errorMessage
        ])
    }
}
```

## Error handling patterns

### Basic error handling

The simplest error handling pattern shows user-friendly messages and logs errors for debugging.

```swift
import PXPCheckoutSDK

let config = CheckoutDropInConfig(
    // ... other config
    onError: { paymentMethod, error in
        // Log error for debugging
        print("Payment error: \(error.errorCode) - \(error.errorMessage)")
        
        // Show user-friendly message
        Task { @MainActor in
            showErrorNotification(error.errorMessage)
            
            // Re-enable payment button
            enablePaymentButton()
        }
    }
)
```

### Advanced error handling with recovery

Implement retry logic, alternative payment methods, and error categorisation.

Don't automatically resubmit payments from `onError` without explicit user confirmation. The following retry logic is for transient network errors only. Payment failures require user action—show an error message and let them manually retry or choose an alternative payment method.

```swift
var retryCount = 0
let maxRetries = 3

let config = CheckoutDropInConfig(
    // ... other config
    onError: { paymentMethod, error in
        // Log to monitoring service
        logErrorToMonitoring([
            "category": "payment_error",
            "error_code": error.errorCode,
            "message": error.errorMessage,
            "device_model": UIDevice.current.model,
            "timestamp": Date().timeIntervalSince1970
        ])
        
        // Track analytics (pseudo-code - use your analytics provider)
        analytics.track("payment_failed", properties: [
            "error_code": error.errorCode,
            "error_message": error.errorMessage,
            "retry_count": retryCount
        ])
        
        // Check if error is retryable (network issues, timeouts only)
        let isNetworkError = error.errorCode == "SDK0500"
        let isTimeout = error.errorMessage.localizedCaseInsensitiveContains("timeout")
        let isRetryable = isNetworkError || isTimeout || 
                         error.errorMessage.localizedCaseInsensitiveContains("unavailable")
        
        // Implement retry logic for transient errors only
        if isRetryable && retryCount < maxRetries {
            retryCount += 1
            
            Task { @MainActor in
                showWarningMessage(
                    "Connection issue (attempt \(retryCount)/\(maxRetries)). " +
                    "Please try your payment again."
                ) {
                    // Retry the payment
                    retryPayment()
                }
            }
            return
        }
        
        // Reset retry count for non-retryable errors
        retryCount = 0
        
        // Show user-friendly error messages based on error code and message
        let userMessage: String
        switch error.errorCode {
        case "SDK0500":
            userMessage = "Network connection issue. Please check your internet connection and try again."
        case "SDK1114":
            userMessage = "3D Secure authentication failed. Please try again or use a different card."
        case "SDK1115":
            // Card authorisation only — not Aeropay (Aeropay missing intent is SDK0115)
            userMessage = "Card authorisation failed. Please check your details and try again."
        case "SDK1116":
            // Card payment failed - check message for specifics
            if error.errorMessage.localizedCaseInsensitiveContains("declined") {
                userMessage = "Your card was declined. Please try a different card or contact your bank."
            } else if error.errorMessage.localizedCaseInsensitiveContains("insufficient funds") {
                userMessage = "Insufficient funds. Please use a different payment method."
            } else if error.errorMessage.localizedCaseInsensitiveContains("expired") {
                userMessage = "This card has expired. Please use a different card."
            } else if error.errorMessage.localizedCaseInsensitiveContains("cvv") ||
                      error.errorMessage.localizedCaseInsensitiveContains("security code") {
                userMessage = "Invalid security code. Please check the CVV on your card and try again."
            } else if error.errorMessage.localizedCaseInsensitiveContains("card number") {
                userMessage = "Invalid card number. Please check and try again."
            } else if error.errorMessage.localizedCaseInsensitiveContains("expiry") ||
                      error.errorMessage.localizedCaseInsensitiveContains("expiration") {
                userMessage = "Invalid expiry date. Please check and try again."
            } else {
                userMessage = "Card payment failed. Please check your details and try again."
            }
        case "SDK1117":
            userMessage = "PayPal payment failed. Please try again or use a different payment method."
        case "SDK1119":
            userMessage = "Apple Pay payment failed. Please try again or use a different payment method."
        case "SDK0114":
            userMessage = "Pay by Bank is only available for e-commerce checkout."
        case "SDK0116":
            userMessage = "Pay by Bank only supports USD."
        case "SDK0113", "SDK0115", "SDK1125":
            userMessage = "Pay by Bank is unavailable. Please choose another payment method."
        case "SDK1300":
            userMessage = "Pay by Bank could not start. Check shopper name, email, and US phone (+1 + 10 digits), or set methodConfig.aeropay.userId."
        case "SDK1126":
            userMessage = "Pay by Bank payment failed. Please try again or use a different payment method."
        case "SDK1303":
            userMessage = "Pay by Bank is not ready. Complete bank verification or choose another method."
        default:
            // Check by message content for scenarios without specific codes
            if error.errorMessage.localizedCaseInsensitiveContains("timeout") {
                userMessage = "Request timed out. Please check your internet connection and try again."
            } else if error.errorMessage.localizedCaseInsensitiveContains("session") ||
                      error.errorMessage.localizedCaseInsensitiveContains("expired") {
                userMessage = "Your payment session has expired. Please reload checkout."
            } else if error.errorMessage.localizedCaseInsensitiveContains("unavailable") {
                userMessage = "Payment service temporarily unavailable. Please try again in a few minutes."
            } else if error.errorCode.hasPrefix("SDK01") || error.errorCode.hasPrefix("SDK02") {
                userMessage = "Payment configuration error. Please contact support."
            } else {
                userMessage = error.errorMessage
            }
        }
        
        Task { @MainActor in
            showErrorMessage(userMessage)
            
            // Offer alternative payment methods for certain errors
            let shouldOfferAlternatives = 
                error.errorCode == "SDK1116" || // Card payment failed
                error.errorCode == "SDK1117" || // PayPal failed
                error.errorCode == "SDK1119" || // Apple Pay failed
                error.errorCode == "SDK1125" || // Aeropay render fallback
                error.errorCode == "SDK0113" || // Aeropay credentials
                error.errorCode == "SDK0115" || // Aeropay intent
                error.errorCode == "SDK1126" || // Aeropay payment failed
                error.errorCode == "SDK1303" ||
                error.errorMessage.localizedCaseInsensitiveContains("declined") ||
                error.errorMessage.localizedCaseInsensitiveContains("insufficient funds")
            
            if shouldOfferAlternatives {
                showAlternativePaymentMethods()
            }
            
            // Show retry button for network/timeout errors
            let shouldShowRetry = 
                error.errorCode == "SDK0500" ||
                error.errorMessage.localizedCaseInsensitiveContains("timeout") ||
                error.errorMessage.localizedCaseInsensitiveContains("unavailable")
            
            if shouldShowRetry {
                showRetryButton()
            }
            
            // Show refresh button for session errors
            if error.errorMessage.localizedCaseInsensitiveContains("session") ||
               error.errorMessage.localizedCaseInsensitiveContains("expired") {
                showRefreshButton()
            }
            
            // Re-enable payment form
            enablePaymentForm()
        }
    }
)
```

### Error logging to monitoring service

Integrate with monitoring services like Firebase Crashlytics or custom logging.

```swift
import FirebaseCrashlytics

let config = CheckoutDropInConfig(
    // ... other config
    onError: { paymentMethod, error in
        // Log to Firebase Crashlytics
        Crashlytics.crashlytics().setCustomValue(error.errorCode, forKey: "error_code")
        Crashlytics.crashlytics().setCustomValue(error.errorMessage, forKey: "error_message")
        Crashlytics.crashlytics().record(
            error: NSError(
                domain: "CheckoutDropIn",
                code: -1,
                userInfo: [
                    NSLocalizedDescriptionKey: "Payment error: \(error.errorCode) - \(error.errorMessage)"
                ]
            )
        )
        
        // Log to custom monitoring service
        Task {
            do {
                try await logErrorToBackend([
                    "type": "payment_error",
                    "error_code": error.errorCode,
                    "message": error.errorMessage,
                    "device_model": UIDevice.current.model,
                    "os_version": UIDevice.current.systemVersion,
                    "timestamp": Date().timeIntervalSince1970
                ])
            } catch {
                // Silently fail - don't disrupt user experience
                print("Failed to log error: \(error.localizedDescription)")
            }
        }
        
        // Show error to user
        Task { @MainActor in
            showErrorMessage(error.errorMessage)
        }
    }
)
```

## Complete error handling example

Here's a production-ready error handling implementation:

```swift
import PXPCheckoutSDK
import FirebaseCrashlytics

var retryCount = 0
let maxRetries = 3

let config = CheckoutDropInConfig(
    // ... other config
    onError: { paymentMethod, error in
        // 1. Log error for debugging
        print("Payment error: \(error.errorCode) - \(error.errorMessage)")
        if let method = paymentMethod {
            print("Payment method: \(method)")
        } else {
            print("No payment method (initialisation/configuration error)")
        }
        
        // 2. Send to monitoring service
        Crashlytics.crashlytics().setCustomValue(error.errorCode, forKey: "error_code")
        Crashlytics.crashlytics().setCustomValue(error.errorMessage, forKey: "error_message")
        Crashlytics.crashlytics().setCustomValue(retryCount, forKey: "retry_count")
        Crashlytics.crashlytics().record(
            error: NSError(
                domain: "CheckoutDropIn",
                code: -1,
                userInfo: [NSLocalizedDescriptionKey: "Payment error: \(error.errorCode)"]
            )
        )
        
        // 3. Track analytics (pseudo-code - use your analytics provider)
        analytics.track("payment_failed", properties: [
            "error_code": error.errorCode,
            "error_message": error.errorMessage,
            "payment_method": paymentMethod?.rawValue ?? "none",
            "retry_count": retryCount,
            "timestamp": Date().timeIntervalSince1970
        ])
        
        // 4. Clear any loading state
        Task { @MainActor in
            hideLoadingOverlay()
        }
        
        // 5. Implement retry logic for transient errors only
        let isNetworkError = error.errorCode == "SDK0500"
        let isTimeout = error.errorMessage.localizedCaseInsensitiveContains("timeout")
        let isUnavailable = error.errorMessage.localizedCaseInsensitiveContains("unavailable")
        
        if (isNetworkError || isTimeout || isUnavailable) && retryCount < maxRetries {
            retryCount += 1
            
            Task { @MainActor in
                showNotification(
                    type: .warning,
                    message: "Connection issue (attempt \(retryCount)/\(maxRetries)). " +
                             "Please try again."
                ) {
                    retryPayment()
                }
            }
            return
        }
        
        // Reset retry count
        retryCount = 0
        
        // 6. Show user-friendly error messages
        let userMessage: String
        switch error.errorCode {
        case "SDK0500":
            userMessage = "Network error. Please check your connection and try again."
        case "SDK1114":
            userMessage = "3D Secure authentication failed. Please try again."
        case "SDK1115":
            // Card authorisation only — not Aeropay (Aeropay missing intent is SDK0115)
            userMessage = "Card authorisation failed. Please check your details and try again."
        case "SDK1116":
            // Card payment failed - check message
            if error.errorMessage.localizedCaseInsensitiveContains("declined") {
                userMessage = "Your card was declined. Please try a different card or contact your bank."
            } else if error.errorMessage.localizedCaseInsensitiveContains("insufficient funds") {
                userMessage = "Insufficient funds. Please use a different payment method."
            } else if error.errorMessage.localizedCaseInsensitiveContains("expired") {
                userMessage = "This card has expired. Please use a different card."
            } else if error.errorMessage.localizedCaseInsensitiveContains("cvv") {
                userMessage = "Invalid security code. Please check and try again."
            } else {
                userMessage = "Card payment failed. Please check your details and try again."
            }
        case "SDK1117":
            userMessage = "PayPal payment failed. Please try again or use a different method."
        case "SDK1119":
            userMessage = "Apple Pay payment failed. Please try again or use a different method."
        case "SDK0114":
            userMessage = "Pay by Bank is only available for e-commerce checkout."
        case "SDK0116":
            userMessage = "Pay by Bank only supports USD."
        case "SDK0113", "SDK0115", "SDK1125":
            userMessage = "Pay by Bank is unavailable. Please choose another payment method."
        case "SDK1300":
            userMessage = "Pay by Bank could not start. Check shopper name, email, and US phone (+1 + 10 digits), or set methodConfig.aeropay.userId."
        case "SDK1126":
            userMessage = "Pay by Bank payment failed. Please try again or use a different payment method."
        case "SDK1303":
            userMessage = "Pay by Bank is not ready. Complete bank verification or choose another method."
        default:
            if error.errorMessage.localizedCaseInsensitiveContains("session") ||
               error.errorMessage.localizedCaseInsensitiveContains("expired") {
                userMessage = "Session expired. Please reload checkout."
            } else if error.errorCode.hasPrefix("SDK01") || error.errorCode.hasPrefix("SDK02") {
                userMessage = "Configuration error. Please contact support."
            } else {
                userMessage = error.errorMessage
            }
        }
        
        // 7. Show error to user
        Task { @MainActor in
            showNotification(
                type: .error,
                title: "Payment failed",
                message: userMessage,
                duration: 8.0
            )
            
            // 8. Offer recovery options
            let shouldOfferAlternatives = 
                error.errorCode == "SDK1116" || 
                error.errorCode == "SDK1117" || 
                error.errorCode == "SDK1119" ||
                error.errorCode == "SDK0113" ||
                error.errorCode == "SDK0115" ||
                error.errorCode == "SDK1125" ||
                error.errorCode == "SDK1126" ||
                error.errorCode == "SDK1303" ||
                error.errorMessage.localizedCaseInsensitiveContains("declined") ||
                error.errorMessage.localizedCaseInsensitiveContains("insufficient funds")
            
            if shouldOfferAlternatives {
                showAlternativePaymentMethods()
            }
            
            let shouldShowRetry = 
                error.errorCode == "SDK0500" ||
                error.errorMessage.localizedCaseInsensitiveContains("timeout")
            
            if shouldShowRetry {
                showRetryButton()
            }
            
            if error.errorMessage.localizedCaseInsensitiveContains("session") ||
               error.errorMessage.localizedCaseInsensitiveContains("expired") {
                showRefreshButton()
            }
            
            // 9. Re-enable UI
            enablePaymentForm()
            enableSubmitButton()
        }
    }
)
```

For specific error codes and exceptions related to individual payment methods, see the payment method documentation: [Card](/guides/checkout/drop-in/ios/cards), [PayPal](/guides/checkout/drop-in/ios/paypal), [Apple Pay](/guides/checkout/drop-in/ios/apple-pay), [Aeropay](/guides/checkout/drop-in/ios/aeropay).