Back to the RevenueCat homepage
RevenueCat SDKGoogle Play Billing

Chapter 8: Error Handling

Handling billing errors directly means dealing with every BillingResponseCode, categorizing them into retriable and non-retriable groups, and building an exponential backoff retry system. Different response codes require different actions: some require reconnection, some require user intervention, some should be silently ignored.

With RevenueCat, you handle one error type: PurchasesError. The SDK has already absorbed the BillingResponseCode layer.

PurchasesError

Every failure in the RevenueCat SDK surfaces as a PurchasesError:

kotlin
public class PurchasesError(
    val code: PurchasesErrorCode,
    val underlyingErrorMessage: String? = null,
) {
    val message: String // human-readable description of the error code
}

PurchasesErrorCode is a cross-platform enum with human-readable codes. The message property on PurchasesError gives a description suitable for logging (not for showing directly to users).

The Error Codes You Handle in Practice

Most of your error handling code deals with these codes:

Code

Meaning

What to do

PurchaseCancelledError

User backed out of the flow

Do nothing; userCancelled will also be true

ProductAlreadyPurchasedError

This product is already active for the user

Refresh CustomerInfo and check entitlements

PaymentPendingError

Purchase entered pending state

Show pending message, wait for UpdatedCustomerInfoListener

NetworkError

Request failed due to connectivity

Ask user to retry

StoreProblemError

Issue with Google Play

Ask user to retry or update Play Store

PurchaseNotAllowedError

Device or user cannot make purchases

Show appropriate message

IneligibleError

User ineligible for the offer

Show base plan instead

Handling Purchase Errors

kotlin
try {
    val result = Purchases.sharedInstance.awaitPurchase(params)
    handleSuccess(result.customerInfo)
} catch (e: PurchasesTransactionException) {
    // PurchasesTransactionException adds userCancelled
    if (e.userCancelled) return

    when (e.error.code) {
        PurchasesErrorCode.PaymentPendingError ->
            showPendingMessage()
        PurchasesErrorCode.ProductAlreadyPurchasedError -> {
            val info = Purchases.sharedInstance.awaitCustomerInfo()
            handleSuccess(info)
        }
        PurchasesErrorCode.NetworkError ->
            showRetryDialog()
        else ->
            showGenericError(e.error.message)
    }
}

PurchasesTransactionException is a subtype of PurchasesException that adds userCancelled: Boolean. Use it for awaitPurchase() only. For awaitRestore(), catch PurchasesException (the base class), restore does not produce a PurchasesTransactionException.

Handling Non-Purchase Errors

For awaitOfferings(), awaitGetProducts(), and awaitCustomerInfo(), catch PurchasesException:

kotlin
try {
    val offerings = Purchases.sharedInstance.awaitOfferings()
    displayOfferings(offerings)
} catch (e: PurchasesException) {
    when (e.error.code) {
        PurchasesErrorCode.NetworkError ->
            showOfflineFallback()
        else ->
            logError(e.error)
    }
}

Retry Logic

You do not write retry logic for transient errors. The SDK retries BillingClient operations internally with backoff for SERVICE_UNAVAILABLE and ERROR response codes. The errors that surface to your code are already past the retry budget.

For NetworkError on RevenueCat API calls (the calls that post purchases or fetch CustomerInfo), the SDK also retries internally. A NetworkError reaching your catch block means retries were exhausted.

The only retries you implement are user-initiated: a "Try Again" button that re-calls the relevant SDK method.

User-Facing Error Messages

Do not show e.error.message to users, it is a technical description. Map to user-facing strings:

kotlin
fun userFacingMessage(error: PurchasesError): String = when (error.code) {
    PurchasesErrorCode.PurchaseCancelledError ->
        "" // show nothing
    PurchasesErrorCode.NetworkError ->
        "Please check your internet connection and try again."
    PurchasesErrorCode.StoreProblemError ->
        "There was a problem with Google Play. Please try again."
    PurchasesErrorCode.ProductAlreadyPurchasedError ->
        "You already have this subscription."
    PurchasesErrorCode.PaymentPendingError ->
        "Your payment is being processed. We'll notify you when it completes."
    else ->
        "Something went wrong. Please try again."
}

Prefer building from scratch?

The Google Play Billing Handbook covers the same topics with raw BillingClient, Developer API, and RTDNs.

Related chapters

  • Chapter 6: The Purchase Flow

    awaitPurchase() handles the complete billing flow, verification, and acknowledgement internally.

    Learn more
  • Chapter 5: Configuring the SDK

    A single configure() call replaces the entire connection lifecycle, reconnection, and sync logic.

    Learn more
  • RevenueCat API Quick Reference

    All SDK methods, data structures, and error codes are fully documented on just a single page.

    Learn more
Error Handling | RevenueCat