Implement robust error handling for seamless payment experiences.
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.
All SDK errors inherit from BaseSdkException, providing a consistent structure:
open class BaseSdkException(
message: String,
cause: Throwable? = null,
val errorCode: String
) : Exception(message, cause)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"
}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)
}
}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)
}
}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.")
}
}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 initialization 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.")
}
}
}
}Handle card tokenisation and storage issues:
class TokenVaultErrorHandler {
fun handleTokenVaultError(exception: BaseSdkException) {
when (exception.errorCode) {
SdkErrorCodes.TOKENIZE_CARD_ERROR -> {
Log.e("TokenVault", "Card tokenization 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.")
}
}
}
}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
))
}
}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...
}
}
)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}"
}
}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 tokenization
Log.w("PaymentFlow", "Tokenization 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()
}
}
}
}
}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
}
}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)
}
}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
}
}
}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()) }
}
}