Skip to content

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.

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

Error callback

Register onError on CheckoutDropInConfig alongside your other callbacks. Use it for every error that happens after you have constructed Drop-in, including render failures during create() and payment failures during checkout.

Reserve do/catch for initialisation only. It catches the two missing-session exceptions above. Do not expect payment or render errors to bubble up as Swift throws.

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
            showErrorMessage(error.errorMessage)
        }
    )
    
    let dropIn = try CheckoutDropIn(config: config)
} 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:

PropertyDescription
errorMessage
String
A human-readable description of what went wrong.
errorCode
String
An SDK code in SDK#### format (for example SDK1114 for authentication failure or SDK0500 for a network error). Card and Paze authorisation failures may also return provider-specific codes through CheckoutDropInFailedSubmitResultSdkException. Use errorCode to branch your handling logic.
paymentMethod
DropInPaymentMethod?
The payment method involved, or nil when the error is not tied to a specific method (for example SDK1100 during create()). Raw values: Card, Paypal, ApplePay.

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 exceptions throw from CheckoutDropIn(config:) when session credentials are missing:

ExceptionError codeDescriptionSolution
MissingConfigSessionIdExceptionSDK0103session.sessionId is empty.Provide a valid SessionData object from your backend.
MissingConfigSessionHmackeyExceptionSDK0104session.hmacKey is empty.Provide a valid HMAC key in SessionData.

Other SDK01xx validation errors (for example missing funding types or intents for a component) surface during create() through onError, often wrapped in a render exception.

Drop-in render and update errors

These exceptions 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.

ExceptionError codeDescriptionSolution
CheckoutDropInConfigNotFoundSdkExceptionSDK1100Failed to retrieve Checkout Drop-In site configuration.Check portal setup and network connectivity; retry create().
CheckoutDropInFailedToRenderCardSdkExceptionSDK1101Failed to render the card component.Check session card configuration and funding types.
CheckoutDropInFailedToRenderPaypalSdkExceptionSDK1102Failed to render the PayPal button.Verify PayPal is enabled in the session.
CheckoutDropInFailedToRenderApplePaySdkExceptionSDK1104Failed to render the Apple Pay button.Check Apple Pay session and device configuration.
CheckoutDropInFailedToRenderCardOnFileSdkExceptionSDK1105Failed to render saved cards.Check token vault and shopper configuration.
CheckoutDropInFailedToRenderBillingAddressSdkExceptionSDK1106Failed to render billing address fields.Review billing address component configuration.
CheckoutDropInFailedToRenderCardBrandSelectorSdkExceptionSDK1107Failed to render the card brand selector.Check accepted card networks in session or config.
CheckoutDropInFailedToRenderCardSubmitSdkExceptionSDK1108Failed to render the card submit button.Check card component wiring and session.
CheckoutDropInFailedToRenderCardCofSubmitSdkExceptionSDK1109Failed to render saved-card submit.Check card-on-file configuration.
CheckoutDropInFailedToRenderCardConsentSdkExceptionSDK1110Failed to render card consent UI.Check site storeCardConsent and shopper setup.
CheckoutDropInFailedToRenderPaypalConsentSdkExceptionSDK1111Failed to render PayPal consent UI.Check PayPal consent configuration.
CheckoutDropInFailedToUpdateBillingAddressSdkExceptionSDK1112Failed to update billing address on the session.Retry or create a new session.
CheckoutDropInFailedToUpdateAllowTransactionSdkExceptionSDK1113Failed to update allow-transaction state (Apple Pay).Retry or create a new session.
CheckoutDropInInvalidPaypalEntryTypeSdkExceptionSDK1120PayPal only supports entryType: .ecom.Change entryType to .ecom.
CheckoutDropInComponentTypeMismatchSdkExceptionSDK1121Internal component type mismatch.Contact support with logs.
NoPaymentMethodsAvailableExceptionSDK0204No payment methods available for the session.Enable at least one funding type in the session response.

Example handling:

onError: { paymentMethod, error in
    if error.errorCode.hasPrefix("SDK110") {
        print("Rendering error: \(error.errorMessage)")
        
        // Log to monitoring
        logger.error("Rendering error", [
            "error_code": error.errorCode,
            "message": error.errorMessage
        ])
        
        // Show generic error to user
        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. Drop-in maps most failures to a method-specific SDK111x code on onError. When the gateway returns provider details, you may receive CheckoutDropInFailedSubmitResultSdkException with a provider-specific code instead of an SDK111x value.

ExceptionError codeDescriptionUser action
CheckoutDropInCardPaymentFailedSdkExceptionSDK1116Card payment failed.See card-specific patterns.
CheckoutDropInPaypalPaymentFailedSdkExceptionSDK1117PayPal transaction failed.Try again or use a different method.
CheckoutDropInApplePayPaymentFailedSdkExceptionSDK1119Apple Pay transaction failed.Try again or use a different method.
CheckoutDropInFailedSubmitResultSdkExceptionProvider code or method fallbackAuthorisation returned a failed submit result with provider details (card, PayPal, Apple Pay).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:

ScenarioDetection approachUser action
Card declinederror.errorMessage contains "declined"Try a different card or contact the bank.
Insufficient fundserror.errorMessage contains "insufficient funds"Use a different payment method.
Expired carderror.errorMessage contains "expired"Use a different card.
Invalid CVVerror.errorMessage contains "CVV" or "security code"Check the security code and retry.
Invalid card numbererror.errorMessage contains "card number" or "invalid number"Check the card number and retry.
Invalid expiryerror.errorMessage contains "expiry" or "expiration"Check the expiry date and retry.

Example handling:

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 error.errorCode == "SDK1123" {
        Task { @MainActor in
            showErrorMessage(
                "Paze payment failed. Please try again or use a different payment method."
            )
            offerAlternativePaymentMethods()
        }
    } else if paymentMethod == .paze {
        Task { @MainActor in
            let userMessage: String
            switch error.errorCode {
            case "SDK1124":
                userMessage = "Paze is only available for e-commerce checkout."
            case "SDK1209", "SDK1210", "SDK1211", "SDK1212":
                userMessage = "Invalid shopper identity: \(error.errorMessage)"
            case "SDK1213":
                userMessage = "Paze is only available for USD transactions."
            case "SDK0118":
                userMessage = "Paze client ID is missing from the session."
            case "SDK1202A":
                userMessage = "Paze merchant category code is missing from the session."
            default:
                userMessage = error.errorMessage.localizedCaseInsensitiveContains("declined")
                    ? "Payment declined. Please try a different payment method."
                    : "Paze payment failed: \(error.errorMessage)"
            }
            showErrorMessage(userMessage)
            if error.errorCode != "SDK1124" {
                offerAlternativePaymentMethods()
            }
        }
    }
}

Authentication errors

These errors relate to 3D Secure (3DS) authentication during card payments. Drop-in surfaces the primary failure as CheckoutDropInAuthenticationFailedSdkException (SDK1114). Related codes from the authentication flow may also appear in error.errorCode or error.errorMessage.

Exception / codeError codeDescriptionUser action
CheckoutDropInAuthenticationFailedSdkExceptionSDK11143DS authentication failed.Try again or use a different card.
Pre-Initiate Authentication failedSDK0502The pre-initiate authentication step failed.Try again or use a different card.
Authentication rejectedSDK0503The issuer or gateway rejected authentication.Try again or use a different card.
Authentication failedSDK0505Authentication did not complete successfully.Try again or use a different card.
Failed to retrieve resultSDK0510The SDK could not retrieve the authentication result.Check the connection and try again.
Challenge page failed to loadSDK0511The 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:

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.

ExceptionError codeDescriptionSolution
CheckoutDropInAuthorisationFailedSdkExceptionSDK1115Payment authorisation failed.Check the transaction details and retry.

Apple Pay errors

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

ExceptionError codeDescriptionSolution
ApplePayPaymentRequestApiNotSupportedExceptionSDK0602Apple Pay is not available on this device or iOS version.Use another payment method; test on a supported device with Apple Pay set up.
ApplePaySessionCancelledExceptionSDK0615Customer cancelled the Apple Pay sheet.No action required — allow retry if needed.
ApplePayMerchantValidationFailedExceptionSDK0616Apple Pay merchant validation failed.Verify Apple Pay merchant configuration in the Unity Portal.
ApplePayCustomApiProcessingFailedExceptionSDK0617Processing 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:

ScenarioHow to detectUser action
Network errorerror.errorCode == "SDK0500"Check the connection and retry.
Session expirederror.errorMessage contains "session" or "expired"Create a new session and reinitialise Drop-in.
Request timeouterror.errorMessage contains "timeout"Check the connection and retry.
Service unavailableerror.errorMessage contains "unavailable" or "service"Try again later.
Configuration errorerror.errorCode starts with "SDK01" or "SDK02"Review your session and config. Contact support if the issue persists.

Example handling:

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.

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.

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":
            userMessage = "Payment 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 "SDK1122":
            userMessage = "Paze is unavailable. Please choose another payment method."
        case "SDK1123":
            userMessage = "Paze payment failed. Please try again or use a different payment method."
        case "SDK1124":
            userMessage = "Paze is only available for e-commerce checkout."
        case "SDK1209", "SDK1210", "SDK1211", "SDK1212":
            userMessage = "Invalid shopper identity: \(error.errorMessage)"
        case "SDK1213":
            userMessage = "Paze is only available for USD transactions."
        case "SDK0118":
            userMessage = "Paze client ID is missing from the session."
        case "SDK1202A":
            userMessage = "Paze merchant category code is missing from the session."
        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 == "SDK1122" || // Paze unavailable
                error.errorCode == "SDK0118" || // Paze clientId missing at create
                error.errorCode == "SDK1123" || // Paze failed
                error.errorCode == "SDK1209" || error.errorCode == "SDK1210" ||
                error.errorCode == "SDK1211" || error.errorCode == "SDK1212" ||
                error.errorCode == "SDK1213" ||
                error.errorCode == "SDK1202A" ||
                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.

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:

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 ?? "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":
            userMessage = "Payment 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 "SDK1122":
            userMessage = "Paze is unavailable. Please choose another payment method."
        case "SDK1123":
            userMessage = "Paze payment failed. Please try again or use a different method."
        case "SDK1124":
            userMessage = "Paze is only available for e-commerce checkout."
        case "SDK1209", "SDK1210", "SDK1211", "SDK1212":
            userMessage = "Invalid shopper identity: \(error.errorMessage)"
        case "SDK1213":
            userMessage = "Paze is only available for USD transactions."
        case "SDK0118":
            userMessage = "Paze client ID is missing from the session."
        case "SDK1202A":
            userMessage = "Paze merchant category code is missing from the session."
        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 if paymentMethod == .paze && error.errorMessage.localizedCaseInsensitiveContains("declined") {
                userMessage = "Payment declined. Please try a different payment method."
            } 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 == "SDK1122" ||
                error.errorCode == "SDK0118" ||
                error.errorCode == "SDK1123" ||
                error.errorCode == "SDK1209" || error.errorCode == "SDK1210" ||
                error.errorCode == "SDK1211" || error.errorCode == "SDK1212" ||
                error.errorCode == "SDK1213" ||
                error.errorCode == "SDK1202A" ||
                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, PayPal, Apple Pay.