
Rc Error Handling
- 188 installs
- 55 repo stars
- Updated August 3, 2026
- revenuecat/ai-toolkit
Helps with ai & agent building tasks.
About
rc-error-handling is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- rc-error-handling
- AI & Agent Building
- AI-coding skill
Rc Error Handling by the numbers
- 188 all-time installs (skills.sh)
- +26 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,978 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/revenuecat/ai-toolkit --skill rc-error-handlingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 188 |
|---|---|
| repo stars | ★ 55 |
| Last updated | August 3, 2026 |
| Repository | revenuecat/ai-toolkit ↗ |
What it does
Helps with ai & agent building tasks.
Files
Error Handling
Phase 1: Understand
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:
PurchasesErrorCodeis a cross platform enum with stable, readable codes.error.messageis a technical string. It belongs in logs, not in the UI.awaitPurchase()throwsPurchasesTransactionException, which adds auserCancelled: Booleanflag.- Every other
await*call (awaitOfferings,awaitGetProducts,awaitCustomerInfo,awaitRestore) throwsPurchasesException. - The SDK already retries transient billing and network failures internally. Any error that reaches you has exhausted the SDK retry budget. You do not add your own backoff loop. The only retry you implement is a user triggered "Try Again" button.
Phase 2: Plan
Before writing a catch block, decide three things:
1. Which await* call are you wrapping? That picks the exception type. 2. Which codes have specific handling? Everything else falls into a generic branch. 3. What user facing string does each handled code map to?
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 |
Phase 3: Execute
Purchase errors
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))
}
}Non purchase errors
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)
}
}Map codes to user facing strings
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."
}Checklist
- You picked
PurchasesTransactionExceptionforawaitPurchaseandPurchasesExceptionelsewhere. - You checked
userCancelledbefore any branching onerror.code. - You handled
PaymentPendingError,ProductAlreadyPurchasedError, andNetworkErrorwith their specific flows. - You logged
error.messageand showed a mapped string fromuserFacingMessageto the user. - You did not add retry loops around SDK calls. Retries are user initiated only.