Real World Example

safeApiCall in an Android Repository

Android apps wrap every Retrofit call so exceptions never reach the UI. The repository returns Result, and the ViewModel turns it into UI state.

class OrderRepository(private val api: OrderApi) {

    suspend fun placeOrder(cart: Cart): Result<OrderConfirmation> = runCatching {
        api.placeOrder(cart.toRequest())
    }.recoverCatching { e ->
        when (e) {
            is HttpException -> if (e.code() == 409) error("Item went out of stock") else throw e
            is IOException -> error("Check your internet connection")
            else -> throw e
        }
    }
}

// ViewModel
viewModelScope.launch {
    repo.placeOrder(cart)
        .onSuccess { _uiState.value = UiState.Success(it) }
        .onFailure { _uiState.value = UiState.Error(it.message ?: "Something went wrong") }
}

Runnable Simulation

Example

A simplified UPI payment flow with typed errors, validation and a single place that converts failures into user messages.

import java.io.IOException

class PaymentDeclinedException(reason: String) : Exception(reason)

fun callBank(upiId: String, amount: Int): String = when {
    upiId.endsWith("@offline") -> throw IOException("Connection reset")
    amount > 50000 -> throw PaymentDeclinedException("Exceeds daily UPI limit")
    else -> "UTR" + (100000 + amount)
}

fun pay(upiId: String, amount: Int): Result<String> = runCatching {
    require('@' in upiId) { "Invalid UPI ID" }
    require(amount > 0) { "Amount must be positive" }
    callBank(upiId, amount)
}

fun userMessage(e: Throwable) = when (e) {
    is IllegalArgumentException -> "Please check: ${e.message}"
    is PaymentDeclinedException -> "Declined by bank: ${e.message}"
    is IOException -> "Network issue. Money not debited. Retry."
    else -> "Unexpected error"
}

fun main() {
    val attempts = listOf("asha@okaxis" to 1500, "ravi" to 200, "kiran@ybl" to 75000, "meena@offline" to 300)
    for ((upi, amt) in attempts) {
        pay(upi, amt).fold(
            onSuccess = { utr -> println("Rs $amt to $upi: SUCCESS ($utr)") },
            onFailure = { e -> println("Rs $amt to $upi: ${userMessage(e)}") },
        )
    }
}

Output

Rs 1500 to asha@okaxis: SUCCESS (UTR101500)
Rs 200 to ravi: Please check: Invalid UPI ID
Rs 75000 to kiran@ybl: Declined by bank: Exceeds daily UPI limit
Rs 300 to meena@offline: Network issue. Money not debited. Retry.