Error Handling
Android SDK: Error Handling
Best practices and patterns for handling errors in the Digital Card Engine SDK, including network failures, authentication issues, and MCD-specific errors.
Error Types
1. CardProviderException
Custom SDK exception for API and authentication errors.
sealed class CardProviderException(message: String) : Exception(message) {
class AuthenticationError(message: String) : CardProviderException(message)
class NetworkError(message: String) : CardProviderException(message)
class ValidationError(message: String) : CardProviderException(message)
class ServerError(message: String) : CardProviderException(message)
}2. MCD Errors
Errors from the MeaWallet MCD SDK when fetching secure card data.
// MCD onFailure callback
override fun onFailure(mcdError: McdError) {
// mcdError.code: Error code
// mcdError.name: Error type name
// mcdError.message: Description
// mcdError.requestId: Request identifier for debugging
}3. Standard Exceptions
IllegalStateException- SDK not initialized, invalid stateIllegalArgumentException- Invalid parametersIOException- Network connectivity issuesSecurityException- Permission issues
Common Error Scenarios
Authentication Errors
Cause: Invalid, expired, or missing authentication token.
Symptoms:
- 401 Unauthorized responses
- "Auth token cannot be empty or blank"
- "Auth token appears to be too short"
Solution:
import com.paymentology.ei_card_provider_sdk.EiCardProviderSDK
class TokenManager(private val sdk: DigitalCardEngineSDK) {
suspend fun refreshTokenIfNeeded() {
try {
val cards = sdk.getCards()
// Success - token valid
} catch (e: CardProviderException.AuthenticationError) {
// Token expired - fetch new one
val newToken = fetchFreshTokenFromBackend()
EiCardProviderSDK.updateAuthToken(newToken)
// Retry operation
val cards = sdk.getCards()
}
}
private suspend fun fetchFreshTokenFromBackend(): String {
// Your backend call to get new token
return myBackendApi.refreshToken()
}
}Network Errors
Cause: No internet connection, timeout, DNS failure.
Symptoms:
SocketTimeoutExceptionUnknownHostExceptionConnectException
Solution:
import androidx.compose.runtime.*
import kotlinx.coroutines.delay
@Composable
fun CardListWithRetry(sdk: DigitalCardEngineSDK) {
var retryCount by remember { mutableStateOf(0) }
var errorMessage by remember { mutableStateOf<String?>(null) }
LaunchedEffect(retryCount) {
try {
val cards = sdk.getCards()
errorMessage = null
} catch (e: IOException) {
errorMessage = "Network error. Please check your connection."
} catch (e: Exception) {
errorMessage = "Error: ${e.message}"
}
}
if (errorMessage != null) {
ErrorView(
message = errorMessage!!,
onRetry = { retryCount++ }
)
} else {
UILibrary.CardListView(sdk = sdk, /* ... */)
}
}MCD Errors (Secure Card Data)
Cause: Missing TOTP secret, card not selected, MCD configuration issues.
Symptoms:
- MCD error codes in logs
- "No selected card" errors
- Secure fields not displaying
Common MCD Error Codes:
| Code | Description | Solution |
|---|---|---|
1001 | Invalid card ID | Verify giftCardId is correct |
1002 | Invalid secret | Check TOTP secret generation |
1003 | Card not found | Ensure card exists and user has access |
2001 | Network error | Check connectivity, retry |
3001 | Invalid response | Contact support |
Solution:
import android.util.Log
lifecycleScope.launch {
try {
// Ensure card is selected
sdk.selectCard(card)
// Fetch secure card images
val images = sdk.getCardImages()
if (images != null) {
panImageView.setImageBitmap(images["pan"])
cvvImageView.setImageBitmap(images["cvv"])
}
} catch (e: IllegalStateException) {
when {
e.message?.contains("No selected card") == true -> {
Log.e("MCD", "Card not selected - call sdk.selectCard() first")
showError("Card selection error")
}
e.message?.contains("MCD error") == true -> {
Log.e("MCD", "MCD SDK error: ${e.message}")
showError("Unable to load secure card details")
}
else -> {
Log.e("MCD", "Unexpected error: ${e.message}")
showError("An error occurred")
}
}
}
}Initialization Errors
Cause: SDK not initialized before use.
Symptoms:
- "SDK not initialized" errors
- Null pointer exceptions
- Crashes on API calls
Solution:
class MyApp : Application() {
lateinit var sdk: DigitalCardEngineSDK
private var isInitialized = false
override fun onCreate() {
super.onCreate()
initializeSDK()
}
private fun initializeSDK() {
try {
sdk = DigitalCardEngineSDK()
val config = DceConfiguration(/* ... */)
sdk.initialize(
context = this,
env = Environment.TEST,
dceConfiguration = config
)
isInitialized = true
} catch (e: Exception) {
Log.e("App", "Failed to initialize SDK: ${e.message}")
// Handle critical error - maybe show error screen
}
}
fun getSdk(): DigitalCardEngineSDK {
if (!isInitialized) {
throw IllegalStateException("SDK not initialized")
}
return sdk
}
}Error Handling Patterns
1. Try-Catch Pattern
Use for synchronous operations or within coroutines.
import kotlinx.coroutines.launch
lifecycleScope.launch {
try {
val cards = sdk.getCards()
updateUI(cards)
} catch (e: CardProviderException.AuthenticationError) {
handleAuthError()
} catch (e: CardProviderException.NetworkError) {
showRetryOption()
} catch (e: Exception) {
logError(e)
showGenericError()
}
}2. Result Pattern
Use for cleaner error propagation.
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
suspend fun fetchCardsResult(): Result<List<CardEntity>> {
return try {
val cards = sdk.getCards()
Result.Success(cards)
} catch (e: Exception) {
Result.Error(e)
}
}
// Usage
when (val result = fetchCardsResult()) {
is Result.Success -> updateUI(result.data)
is Result.Error -> handleError(result.exception)
}3. State Pattern (Compose)
Use for UI state management.
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
class CardListViewModel(private val sdk: DigitalCardEngineSDK) : ViewModel() {
var uiState by mutableStateOf<UiState<List<CardEntity>>>(UiState.Loading)
private set
fun loadCards() {
uiState = UiState.Loading
viewModelScope.launch {
uiState = try {
val cards = sdk.getCards()
UiState.Success(cards)
} catch (e: Exception) {
UiState.Error(e.message ?: "Unknown error")
}
}
}
}
// Usage in Compose
@Composable
fun CardListScreen(viewModel: CardListViewModel) {
when (val state = viewModel.uiState) {
is UiState.Loading -> LoadingIndicator()
is UiState.Success -> CardList(state.data)
is UiState.Error -> ErrorView(state.message) {
viewModel.loadCards()
}
}
}Retry Strategies
Exponential Backoff
import kotlinx.coroutines.delay
import kotlin.math.pow
suspend fun <T> retryWithBackoff(
maxRetries: Int = 3,
initialDelayMs: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelayMs
var lastException: Exception? = null
repeat(maxRetries) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
if (attempt < maxRetries - 1) {
Log.w("Retry", "Attempt ${attempt + 1} failed, retrying in ${currentDelay}ms")
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong()
}
}
}
throw lastException ?: Exception("Max retries exceeded")
}
// Usage
lifecycleScope.launch {
try {
val cards = retryWithBackoff {
sdk.getCards()
}
updateUI(cards)
} catch (e: Exception) {
showError("Failed after multiple retries: ${e.message}")
}
}Conditional Retry
fun shouldRetry(exception: Exception): Boolean {
return when (exception) {
is IOException -> true // Network errors
is CardProviderException.NetworkError -> true
is CardProviderException.AuthenticationError -> false // Don't retry auth errors
else -> false
}
}
suspend fun <T> retryIfAppropriate(
maxRetries: Int = 3,
block: suspend () -> T
): T {
var lastException: Exception? = null
repeat(maxRetries) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
if (!shouldRetry(e) || attempt >= maxRetries - 1) {
throw e
}
delay(1000L * (attempt + 1))
}
}
throw lastException ?: Exception("Max retries exceeded")
}Logging Best Practices
Structured Logging
import android.util.Log
object SdkLogger {
private const val TAG = "DigitalCardEngine"
fun logApiCall(endpoint: String, params: Map<String, Any>? = null) {
Log.d(TAG, "API Call: $endpoint ${params?.let { "with $it" } ?: ""}")
}
fun logApiSuccess(endpoint: String, duration: Long) {
Log.d(TAG, "API Success: $endpoint (${duration}ms)")
}
fun logApiError(endpoint: String, error: Exception) {
Log.e(TAG, "API Error: $endpoint - ${error.message}", error)
}
fun logMcdError(error: McdError) {
Log.e(TAG, "MCD Error [${error.code}]: ${error.name} - ${error.message} (reqId: ${error.requestId})")
}
}
// Usage
try {
SdkLogger.logApiCall("getCards")
val startTime = System.currentTimeMillis()
val cards = sdk.getCards()
val duration = System.currentTimeMillis() - startTime
SdkLogger.logApiSuccess("getCards", duration)
} catch (e: Exception) {
SdkLogger.logApiError("getCards", e)
throw e
}User-Friendly Error Messages
fun getUser FriendlyMessage(exception: Exception): String {
return when (exception) {
is CardProviderException.AuthenticationError ->
"Your session has expired. Please log in again."
is CardProviderException.NetworkError ->
"Unable to connect. Please check your internet connection and try again."
is CardProviderException.ServerError ->
"Our servers are experiencing issues. Please try again later."
is IOException ->
"Connection problem. Please check your network and retry."
is IllegalStateException -> when {
exception.message?.contains("No selected card") == true ->
"Card selection error. Please try again."
exception.message?.contains("MCD error") == true ->
"Unable to display secure card information."
else ->
"An unexpected error occurred."
}
else ->
"Something went wrong. Please try again."
}
}
// Usage
@Composable
fun ErrorView(exception: Exception, onRetry: () -> Unit) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = getUserFriendlyMessage(exception),
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onRetry) {
Text("Try Again")
}
}
}Complete Example
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class CardViewModel(private val sdk: DigitalCardEngineSDK) : ViewModel() {
var state by mutableStateOf<CardState>(CardState.Loading)
private set
sealed class CardState {
object Loading : CardState()
data class Success(val cards: List<CardEntity>) : CardState()
data class Error(val message: String, val canRetry: Boolean) : CardState()
}
init {
loadCards()
}
fun loadCards() {
state = CardState.Loading
viewModelScope.launch {
try {
val cards = retryWithBackoff(maxRetries = 2) {
sdk.getCards()
}
state = CardState.Success(cards)
} catch (e: CardProviderException.AuthenticationError) {
// Try to refresh token
try {
val newToken = refreshToken()
EiCardProviderSDK.updateAuthToken(newToken)
loadCards() // Retry after token refresh
} catch (tokenError: Exception) {
state = CardState.Error(
message = "Authentication failed. Please log in again.",
canRetry = false
)
}
} catch (e: IOException) {
state = CardState.Error(
message = "Network error. Check your connection.",
canRetry = true
)
} catch (e: Exception) {
Log.e("CardViewModel", "Unexpected error", e)
state = CardState.Error(
message = "An error occurred: ${e.message}",
canRetry = true
)
}
}
}
private suspend fun refreshToken(): String {
// Your token refresh logic
throw NotImplementedError("Implement token refresh")
}
}
@Composable
fun CardScreen(viewModel: CardViewModel) {
when (val state = viewModel.state) {
is CardViewModel.CardState.Loading -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is CardViewModel.CardState.Success -> {
CardList(cards = state.cards)
}
is CardViewModel.CardState.Error -> {
ErrorView(
message = state.message,
canRetry = state.canRetry,
onRetry = if (state.canRetry) {
{ viewModel.loadCards() }
} else null
)
}
}
}Troubleshooting Checklist
SDK Initialization Issues:
- SDK initialized in Application.onCreate()
- Valid token provided
- Correct environment selected (DEV/TEST/PROD)
- Internet permission in manifest
Authentication Errors:
- Token is not expired
- Token format is correct
- Token refresh logic implemented
- Backend issuing valid tokens
Network Errors:
- Device has internet connectivity
- Firewall not blocking requests
- Correct API endpoints configured
- SSL certificates valid
MCD Errors:
- MCD artifact added to dependencies
- MCD credentials configured in gradle.properties
- Card selected before fetching images
- TOTP secret available and valid
- Correct MCD environment for your build
See Also
- Initialize - Proper SDK initialization
- Card List - Handling errors in card lists
- Card Details - MCD error handling
- Data Models - Understanding SDK data structures
Updated 3 months ago
