Skip to content

Error handling

Implement robust error handling for seamless payment experiences.

Overview

The Android SDK provides comprehensive error handling mechanisms to help you create resilient payment experiences.

By implementing proper error handling, you can:

  • Provide clear user feedback with meaningful error messages.
  • Implement retry logic for transient failures.
  • Track and diagnose issues for improved reliability.
  • Handle edge cases gracefully without crashes.
  • Maintain payment flow continuity through error recovery.

The SDK uses a hierarchical error system with standardised error codes, making it easy to handle different error types consistently across your application.

Error architecture

Error hierarchy

All SDK errors inherit from BaseSdkException, providing a consistent structure:

open class BaseSdkException(
    message: String,
    cause: Throwable? = null,
    val errorCode: String
) : Exception(message, cause)

Error categories

The SDK organises errors into logical categories with standardised codes:

object SdkErrorCodes {
    // 00: Common Errors
    const val UNEXPECTED_ERROR = "SDK0000"
    const val NOT_IMPLEMENTED = "SDK0001"
    const val VALIDATION_ERROR = "SDK0002"
    
    // 01: SDK Errors
    const val SDK_CONFIG_EMPTY = "SDK0100"
    const val SDK_CONFIG_ENVIRONMENT_EMPTY = "SDK0101"
    const val SDK_CONFIG_SESSION_EMPTY = "SDK0102"
    
    // 02: Component Errors
    const val COMPONENT_ERROR = "SDK0200"
    const val COMPONENT_CONFIG_NAME_EMPTY = "SDK0201"
    const val COMPONENT_CONTAINER_NOT_FOUND = "SDK0202"
    
    // 03: Token Vault Errors
    const val TOKEN_VAULT_ERROR = "SDK0300"
    const val TOKENIZE_CARD_ERROR = "SDK0301"
    const val RETRIEVE_TOKENS_ERROR = "SDK0302"
    
    // 04: ThreeDSecure Errors
    const val AUTHENTICATION_FAILED = "SDK0505"
    const val PRE_INITIATE_INTEGRATED_AUTHENTICATION_CURRENCY_CODE_INVALID = "SDK0400"
    
    // 05: Transaction Errors
    const val NETWORK_ERROR = "SDK0500"
    const val VALIDATION_ERROR = "SDK0501"
    const val PARSE_ERROR = "SDK0502"
}

Common error types

Validation errors

Handle validation failures for user input:

class ValidationErrorHandler {
    
    fun handleValidationError(exception: ComponentValidationException) {
        when (exception.errorCode) {
            SdkErrorCodes.VALIDATION_ERROR -> {
                // Display field-specific validation messages
                exception.validationResults.forEach { result ->
                    if (!result.isValid) {
                        showFieldError(result.fieldName, result.errorMessage)
                    }
                }
            }
            SdkErrorCodes.MISSING_REQUIRED_FIELD -> {
                showError("Please fill in all required fields")
            }
            SdkErrorCodes.INVALID_INPUT -> {
                showError("Please check your input and try again")
            }
        }
    }
    
    private fun showFieldError(fieldName: String, message: String) {
        Log.e("Validation", "Field '$fieldName': $message")
        // Update UI to show field-specific error
        updateFieldErrorState(fieldName, message)
    }
}

Network errors

Handle connectivity and API-related issues:

class NetworkErrorHandler {
    
    fun handleNetworkError(exception: BaseSdkException) {
        when (exception.errorCode) {
            ErrorCode.NETWORK_ERROR -> {
                Log.e("Network", "Network connection error: ${exception.message}")
                showRetryableError("No internet connection. Please check your connection and try again.")
            }
            ErrorCode.TIMEOUT_ERROR -> {
                Log.e("Network", "Request timeout: ${exception.message}")
                showRetryableError("Request timed out. Please try again.")
            }
            ErrorCode.HTTP_ERROR -> {
                Log.e("Network", "HTTP error: ${exception.message}")
                showError("Server error. Please try again later.")
            }
            ErrorCode.API_ERROR -> {
                Log.e("Network", "API error: ${exception.message}")
                showError("Service temporarily unavailable. Please try again.")
            }
            else -> {
                Log.e("Network", "Unknown network error: ${exception.message}")
                showError("Connection error. Please try again.")
            }
        }
    }
    
    private fun showRetryableError(message: String) {
        // Show error with retry button
        showErrorDialog(message, showRetryButton = true)
    }
}

3DS authentication errors

Handle 3D Secure authentication failures:

class ThreeDSErrorHandler {
    
    fun handleAuthenticationError(exception: BaseSdkException) {
        when (exception.errorCode) {
            "SDK0505" -> { // AUTHENTICATION_FAILED
                Log.e("3DS", "Authentication failed: ${exception.message}")
                showError("Authentication failed. Please try again or contact your bank.")
            }
            "SDK0502" -> { // PRE_INITIATE_AUTHENTICATION_FAILED
                Log.e("3DS", "Pre-initiate authentication failed: ${exception.message}")
                showError("Unable to start authentication. Please try again.")
            }
            "SDK0503" -> { // TRANSACTION_AUTHENTICATION_REJECTED
                Log.e("3DS", "Authentication rejected: ${exception.message}")
                showError("Authentication was rejected. Please contact your bank.")
            }
            "SDK0504" -> { // TRANSACTION_AUTHENTICATION_REQUIRE_SCA_EXEMPTION
                Log.e("3DS", "SCA exemption required: ${exception.message}")
                handleScaExemptionRequired()
            }
            else -> {
                Log.e("3DS", "Unknown authentication error: ${exception.message}")
                showError("Authentication error. Please try again.")
            }
        }
    }
    
    private fun handleScaExemptionRequired() {
        // Handle SCA exemption requirement
        showError("Additional authentication required. Please contact support.")
    }
}

Component errors

Handle component initialisation and lifecycle errors:

class ComponentErrorHandler {
    
    fun handleComponentError(exception: ComponentException) {
        Log.e("Component", "Component error in ${exception.componentType}: ${exception.message}")
        
        when (exception.errorCode) {
            SdkErrorCodes.COMPONENT_ERROR -> {
                showError("Component initialisation failed. Please refresh and try again.")
            }
            SdkErrorCodes.COMPONENT_CONFIG_NAME_EMPTY -> {
                Log.e("Component", "Component configuration error: missing name")
                showError("Configuration error. Please contact support.")
            }
            SdkErrorCodes.COMPONENT_CONTAINER_NOT_FOUND -> {
                Log.e("Component", "Component container not found")
                showError("UI error. Please refresh and try again.")
            }
            else -> {
                showError("Component error. Please try again.")
            }
        }
    }
}

Token Vault errors

Handle card tokenisation and storage issues:

class TokenVaultErrorHandler {
    
    fun handleTokenVaultError(exception: BaseSdkException) {
        when (exception.errorCode) {
            SdkErrorCodes.TOKENIZE_CARD_ERROR -> {
                Log.e("TokenVault", "Card tokenisation failed: ${exception.message}")
                showError("Unable to process card details. Please check your information and try again.")
            }
            SdkErrorCodes.RETRIEVE_TOKENS_ERROR -> {
                Log.e("TokenVault", "Failed to retrieve saved cards: ${exception.message}")
                showError("Unable to load saved payment methods. Please try again.")
            }
            SdkErrorCodes.DELETE_TOKEN_ERROR -> {
                Log.e("TokenVault", "Failed to delete saved card: ${exception.message}")
                showError("Unable to remove payment method. Please try again.")
            }
            SdkErrorCodes.UPDATE_TOKEN_ERROR -> {
                Log.e("TokenVault", "Failed to update saved card: ${exception.message}")
                showError("Unable to update payment method. Please try again.")
            }
            SdkErrorCodes.ENCRYPTION_ERROR -> {
                Log.e("TokenVault", "Encryption error: ${exception.message}")
                showError("Security error. Please try again.")
            }
            else -> {
                Log.e("TokenVault", "Unknown token vault error: ${exception.message}")
                showError("Payment method error. Please try again.")
            }
        }
    }
}

Implementation patterns

Centralised error handling

Create a central error handler for consistent error management:

class PxpErrorHandler {
    
    fun handleError(error: Throwable) {
        val baseSdkException = when (error) {
            is BaseSdkException -> error
            is Exception -> {
                if (error.message == "NetWorkError") {
                    NetworkSdkException(error)
                } else {
                    UnexpectedSdkException(error)
                }
            }
            else -> UnexpectedSdkException(error)
        }
        
        when (baseSdkException) {
            is ComponentValidationException -> ValidationErrorHandler().handleValidationError(baseSdkException)
            is AuthenticationFailedException -> ThreeDSErrorHandler().handleAuthenticationError(baseSdkException)
            is ComponentException -> ComponentErrorHandler().handleComponentError(baseSdkException)
            else -> handleGenericError(baseSdkException)
        }
        
        // Track error for analytics
        trackError(baseSdkException)
    }
    
    private fun handleGenericError(exception: BaseSdkException) {
        Log.e("PxpError", "Generic error: ${exception.message} (${exception.errorCode})")
        showError("An error occurred. Please try again.")
    }
    
    private fun trackError(exception: BaseSdkException) {
        // Track error for analytics and monitoring
        analytics.track("payment_error", mapOf(
            "error_code" to exception.errorCode,
            "error_message" to exception.message,
            "error_type" to exception::class.simpleName
        ))
    }
}

Component-specific error handling

Implement error handling in component configurations:

val cardSubmitConfig = CardSubmitComponentConfig(
    // Handle submission errors
    onSubmitError = { exception ->
        when (exception.errorCode) {
            SdkErrorCodes.VALIDATION_ERROR -> {
                Log.e("CardSubmit", "Validation failed: ${exception.message}")
                showValidationErrors()
            }
            ErrorCode.NETWORK_ERROR -> {
                Log.e("CardSubmit", "Network error during submission: ${exception.message}")
                showRetryOption("Network error. Please check your connection and try again.")
            }
            else -> {
                Log.e("CardSubmit", "Submission error: ${exception.message}")
                showError("Payment failed. Please try again.")
            }
        }
    },
    
    // Handle validation results
    onValidation = { validationResults ->
        val hasErrors = validationResults.any { !it.isValid }
        if (hasErrors) {
            handleValidationErrors(validationResults)
        } else {
            clearValidationErrors()
        }
    },
    
    // Handle authorisation results
    onPostAuthorisation = { result ->
        when (result) {
            is FailedSubmitResult -> {
                Log.e("CardSubmit", "Payment failed: ${result.errorReason}")
                handlePaymentFailure(result)
            }
            is RefusedSubmitResult -> {
                Log.w("CardSubmit", "Payment refused: ${result.stateData.message}")
                handlePaymentRefused(result)
            }
            // Handle success cases...
        }
    }
)

Retry mechanisms

Implement intelligent retry logic for transient failures:

class PaymentRetryManager {
    private var retryCount = 0
    private val maxRetries = 3
    private val retryDelays = listOf(1000L, 2000L, 5000L) // Progressive delays
    
    fun handleRetryableError(error: BaseSdkException, retryAction: () -> Unit) {
        if (shouldRetry(error) && retryCount < maxRetries) {
            val delay = retryDelays.getOrElse(retryCount) { 5000L }
            retryCount++
            
            Log.w("Retry", "Retrying operation in ${delay}ms (attempt $retryCount/$maxRetries)")
            showRetryMessage("Retrying... (${retryCount}/$maxRetries)")
            
            // Delay and retry
            Handler(Looper.getMainLooper()).postDelayed({
                retryAction()
            }, delay)
        } else {
            // Max retries reached or non-retryable error
            retryCount = 0
            handleFinalFailure(error)
        }
    }
    
    private fun shouldRetry(error: BaseSdkException): Boolean {
        return when (error.errorCode) {
            ErrorCode.NETWORK_ERROR,
            ErrorCode.TIMEOUT_ERROR,
            ErrorCode.HTTP_ERROR -> true
            else -> false
        }
    }
    
    private fun handleFinalFailure(error: BaseSdkException) {
        Log.e("Retry", "Max retries reached for error: ${error.message}")
        showError("Unable to complete payment after multiple attempts. Please try again later.")
    }
    
    fun resetRetryCount() {
        retryCount = 0
    }
}

###User-friendly error messages

Convert technical errors into user-friendly messages:

class ErrorMessageFormatter {
    
    fun formatUserMessage(exception: BaseSdkException): String {
        return when (exception.errorCode) {
            // Validation errors
            SdkErrorCodes.VALIDATION_ERROR -> "Please check your payment details and try again."
            "CARD_NUMBER_INVALID" -> "Please enter a valid card number."
            "CARD_EXPIRED" -> "This card has expired. Please use a different card."
            "CVC_INVALID" -> "Please enter a valid security code."
            
            // Network errors
            ErrorCode.NETWORK_ERROR -> "No internet connection. Please check your connection and try again."
            ErrorCode.TIMEOUT_ERROR -> "Request timed out. Please try again."
            
            // 3DS errors
            "SDK0505" -> "Card authentication failed. Please contact your bank for assistance."
            "SDK0503" -> "Authentication was declined. Please contact your bank."
            
            // Token vault errors
            SdkErrorCodes.TOKENIZE_CARD_ERROR -> "Unable to process card details. Please try again."
            SdkErrorCodes.RETRIEVE_TOKENS_ERROR -> "Unable to load saved payment methods."
            
            // Generic fallbacks
            else -> "Payment failed. Please try again or contact support."
        }
    }
    
    fun formatDeveloperMessage(exception: BaseSdkException): String {
        return "Error ${exception.errorCode}: ${exception.message}"
    }
}

Error recovery strategies

Graceful degradation

Implement fallback options when primary payment methods fail:

class PaymentFlowManager {
    
    fun handlePaymentFailure(error: BaseSdkException) {
        when (error.errorCode) {
            SdkErrorCodes.TOKENIZE_CARD_ERROR -> {
                // Fallback to direct payment without tokenisation
                Log.w("PaymentFlow", "Tokenisation failed, proceeding without saving card")
                showOption("Payment method won't be saved for future use. Continue?") {
                    proceedWithDirectPayment()
                }
            }
            "SDK0505" -> { // 3DS authentication failed
                // Fallback to non-3DS if supported
                Log.w("PaymentFlow", "3DS failed, attempting non-3DS payment")
                showOption("Authentication failed. Try simplified payment?") {
                    proceedWithNon3DSPayment()
                }
            }
            SdkErrorCodes.RETRIEVE_TOKENS_ERROR -> {
                // Fallback to new card entry
                Log.w("PaymentFlow", "Saved cards unavailable, using new card flow")
                showNewCardForm()
            }
            else -> {
                // Generic retry option
                showRetryOption("Payment failed. Would you like to try again?") {
                    retryPayment()
                }
            }
        }
    }
}

Progressive error handling

Implement escalating error handling based on failure count:

class ProgressiveErrorHandler {
    private var consecutiveFailures = 0
    
    fun handleError(error: BaseSdkException) {
        consecutiveFailures++
        
        when (consecutiveFailures) {
            1 -> {
                // First failure: Simple retry
                showSimpleRetry("Payment failed. Please try again.")
            }
            2 -> {
                // Second failure: Suggest checking details
                showDetailedRetry("Payment failed again. Please check your card details and try again.")
            }
            3 -> {
                // Third failure: Offer alternative payment methods
                showAlternativeOptions("Multiple payment failures. Would you like to try a different payment method?")
            }
            else -> {
                // Multiple failures: Suggest contacting support
                showSupportOption("We're having trouble processing your payment. Please contact support for assistance.")
            }
        }
    }
    
    fun resetFailureCount() {
        consecutiveFailures = 0
    }
}

Best practices

Error logging and monitoring

Implement comprehensive error tracking:

class ErrorTracker {
    
    fun trackError(error: BaseSdkException, context: Map<String, Any> = emptyMap()) {
        // Log to console for debugging
        Log.e("PxpError", "Error ${error.errorCode}: ${error.message}", error)
        
        // Send to crash reporting
        crashlytics.recordException(error)
        
        // Track in analytics
        analytics.track("payment_error", mapOf(
            "error_code" to error.errorCode,
            "error_message" to error.message,
            "error_type" to error::class.simpleName,
            "timestamp" to System.currentTimeMillis(),
            "user_id" to getCurrentUserId(),
            "session_id" to getCurrentSessionId()
        ) + context)
        
        // Send to monitoring service
        monitoringService.recordError(error, context)
    }
}

User experience considerations

Prioritise user experience in error handling:

class UserExperienceErrorHandler {
    
    fun handleErrorWithUX(error: BaseSdkException) {
        // Hide loading indicators
        hideLoadingStates()
        
        // Vibrate for tactile feedback on errors
        if (shouldVibrateOnError(error)) {
            vibrate(100)
        }
        
        // Choose appropriate display method
        when (error.errorCode) {
            SdkErrorCodes.VALIDATION_ERROR -> {
                // Show inline field errors
                showInlineErrors()
            }
            ErrorCode.NETWORK_ERROR -> {
                // Show toast for temporary issues
                showToast(formatUserMessage(error))
            }
            else -> {
                // Show dialog for serious errors
                showErrorDialog(formatUserMessage(error))
            }
        }
        
        // Re-enable form interactions
        enableFormInteractions()
    }
    
    private fun shouldVibrateOnError(error: BaseSdkException): Boolean {
        // Only vibrate for user-actionable errors
        return when (error.errorCode) {
            SdkErrorCodes.VALIDATION_ERROR,
            "CARD_NUMBER_INVALID",
            "CVC_INVALID" -> true
            else -> false
        }
    }
}

Testing error scenarios

Test error handling thoroughly:

class ErrorHandlingTests {
    
    @Test
    fun testValidationErrorHandling() {
        // Simulate validation error
        val validationError = ComponentValidationException(
            componentType = "CardNumber",
            validationResults = listOf(
                ValidationResult(
                    fieldName = "cardNumber",
                    isValid = false,
                    errorMessage = "Invalid card number"
                )
            )
        )
        
        errorHandler.handleError(validationError)
        
        // Verify UI shows error
        verify { mockUI.showFieldError("cardNumber", "Invalid card number") }
    }
    
    @Test
    fun testNetworkErrorRetry() {
        val networkError = NetworkSdkException("Connection timeout")
        
        errorHandler.handleError(networkError)
        
        // Verify retry mechanism is triggered
        verify { mockRetryManager.scheduleRetry(any()) }
    }
}