Use this skill when handling errors from the RevenueCat Android SDK. Covers PurchasesError, the PurchasesErrorCode enum, the userCancelled flag on PurchasesTransactionException, and the recommended UI response per code.
71
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
With raw Google Play Billing you enumerate every BillingResponseCode, split them into retriable and non retriable groups, and build backoff retry logic. RevenueCat collapses this into a single type you deal with: PurchasesError.
public class PurchasesError(
val code: PurchasesErrorCode,
val underlyingErrorMessage: String? = null,
) {
val message: String // technical description, for logs
}Key facts you rely on:
PurchasesErrorCode is a cross platform enum with stable, readable codes.error.message is a technical string. It belongs in logs, not in the UI.awaitPurchase() throws PurchasesTransactionException, which adds a userCancelled: Boolean flag.await* call (awaitOfferings, awaitGetProducts, awaitCustomerInfo, awaitRestore) throws PurchasesException.Before writing a catch block, decide three things:
await* call are you wrapping? That picks the exception type.Use this table to categorize PurchasesErrorCode values and pick the UX response.
| Code | Meaning | Handling |
|---|---|---|
PurchaseCancelledError | User backed out of the flow | Do nothing. userCancelled is also true. |
ProductAlreadyPurchasedError | Product already active for the user | Refresh CustomerInfo and check entitlements. |
PaymentPendingError | Purchase entered pending state | Show a pending message. Wait for UpdatedCustomerInfoListener. |
NetworkError | Request failed due to connectivity | Prompt the user to retry. |
StoreProblemError | Google Play issue | Prompt to retry or update Play Store. |
PurchaseNotAllowedError | Device or account cannot purchase | Show an explanatory message. |
IneligibleError | User not eligible for the offer | Show the base plan instead. |
Exception type decision:
| Call | Exception to catch | userCancelled available? |
|---|---|---|
awaitPurchase() | PurchasesTransactionException | Yes |
awaitRestore() | PurchasesException | No |
awaitOfferings() | PurchasesException | No |
awaitGetProducts() | PurchasesException | No |
awaitCustomerInfo() | PurchasesException | No |
Check userCancelled first and return silently. Then branch on error.code.
try {
val result = Purchases.sharedInstance.awaitPurchase(params)
handleSuccess(result.customerInfo)
} catch (e: PurchasesTransactionException) {
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(userFacingMessage(e.error))
}
}Catch PurchasesException and branch on the code. Use offline or cached fallbacks where you have them.
try {
val offerings = Purchases.sharedInstance.awaitOfferings()
displayOfferings(offerings)
} catch (e: PurchasesException) {
when (e.error.code) {
PurchasesErrorCode.NetworkError -> showOfflineFallback()
else -> logError(e.error)
}
}Keep a single mapping function. Never pass e.error.message to the UI.
fun userFacingMessage(error: PurchasesError): String = when (error.code) {
PurchasesErrorCode.PurchaseCancelledError -> ""
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."
}PurchasesTransactionException for awaitPurchase and PurchasesException elsewhere.userCancelled before any branching on error.code.PaymentPendingError, ProductAlreadyPurchasedError, and NetworkError with their specific flows.error.message and showed a mapped string from userFacingMessage to the user.ccfc038
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.