RevenueCat Error Codes (PurchasesErrorCode)

Overview

RevenueCat surfaces a typed error code on every failure. Instead of parsing free-form message strings, you switch on a named value and respond to each case correctly. The exact type depends on the platform:

  • iOS (Swift): errors bridge to the RevenueCat.ErrorCode enum, for example .purchaseCancelledError, .networkError, .configurationError.
  • Android (Kotlin): a PurchasesError exposes a .code property of type PurchasesErrorCode, for example PurchasesErrorCode.PurchaseCancelledError, NetworkError, ConfigurationError.
  • React Native: the caught error exposes a string error.code from the PURCHASES_ERROR_CODE enum, for example 'PURCHASE_CANCELLED_ERROR', plus a convenient error.userCancelled boolean.

The single most important practical rule: always check the user cancelled flag first. When a user simply backs out of the purchase sheet, that is not a failure to surface. Return early and show nothing.

SDK codes vs backend codes. The codes on this page are SDK-side PurchasesErrorCode values. RevenueCat also returns a separate set of backend error codes from its REST API. For the authoritative, always-current list of both, see the official RevenueCat errors documentation.

Handle Errors in React Native

In react-native-purchases, purchase and configuration calls reject, so wrap them in try/catch. The caught error object exposes error.code (a string from the PURCHASES_ERROR_CODE enum), error.userCancelled (a boolean), error.message, and error.underlyingErrorMessage:

typescript
import Purchases, { PURCHASES_ERROR_CODE } from 'react-native-purchases';

async function buy(pkg) {
  try {
    await Purchases.purchasePackage(pkg);
    // Purchase succeeded.
  } catch (e: any) {
    if (e.userCancelled) return;            // user backed out, not an error to surface

    console.log(e.code, e.message);         // e.code is a PURCHASES_ERROR_CODE string

    switch (e.code) {
      case PURCHASES_ERROR_CODE.NETWORK_ERROR:
        // Transient: offer a retry.
        break;
      case PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR:
        // Already owned: try Purchases.restorePurchases().
        break;
      case PURCHASES_ERROR_CODE.CONFIGURATION_ERROR:
        // Developer setup bug: fix product / API key configuration.
        break;
      default:
        // Fall back to a friendly generic message.
        // e.underlyingErrorMessage often has the store-level detail.
        break;
    }
  }
}
Import the enum to compare safely. Import PURCHASES_ERROR_CODE from react-native-purchases and compare against its members rather than hardcoding the string literals. This keeps your switch resilient to typos and easy to read.

Handle Errors in iOS

In Swift, RevenueCat errors bridge to the RevenueCat.ErrorCode enum. With the async API, catch the thrown error and cast it so you can switch on the specific case:

swift
import RevenueCat

func buy(_ pkg: Package) async {
    do {
        _ = try await Purchases.shared.purchase(package: pkg)
        // Purchase succeeded.
    } catch {
        if let rcError = error as? RevenueCat.ErrorCode {
            switch rcError {
            case .purchaseCancelledError:
                // User backed out: not an error to surface.
                return
            case .networkError:
                // Transient: offer a retry.
                break
            case .configurationError:
                // Developer setup bug: fix product / API key configuration.
                break
            default:
                // Fall back to a friendly generic message.
                break
            }
        }
    }
}
Detecting cancellation on iOS. When the user dismisses the purchase sheet, the error bridges to the .purchaseCancelledError case, so check for it first and return. If you are unsure of the exact cast in your SDK version, keep it simple and consult the RevenueCat errors docs.

Handle Errors in Android

On Android, the callback and coroutine APIs surface a PurchasesError with a .code property of type PurchasesErrorCode. The purchaseWith onError callback also hands you a separate userCancelled boolean, so you can short-circuit before inspecting the code:

kotlin
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesErrorCode

Purchases.sharedInstance.purchaseWith(
    params,
    onError = { error, userCancelled ->
        if (userCancelled) return@purchaseWith   // user backed out, not an error

        when (error.code) {
            PurchasesErrorCode.NetworkError -> {
                // Transient: offer a retry.
            }
            PurchasesErrorCode.ProductAlreadyPurchasedError -> {
                // Already owned: try restorePurchases().
            }
            PurchasesErrorCode.ConfigurationError -> {
                // Developer setup bug: fix product / API key configuration.
            }
            else -> {
                // Fall back to a friendly generic message.
            }
        }
    },
    onSuccess = { _, customerInfo ->
        // Purchase succeeded.
    }
)

Common Error Codes

These are real RevenueCat PurchasesErrorCode names. The casing shown matches the Android enum; React Native uses the SCREAMING_SNAKE_CASE form (for example PURCHASE_CANCELLED_ERROR) and iOS uses the lowerCamelCase case (for example .purchaseCancelledError). The meaning is the same across platforms.

PurchasesErrorCode What it means Typical fix
PurchaseCancelledError The user dismissed the purchase sheet. Not a real error. Check the cancelled flag first and return early.
StoreProblemError A problem occurred with the App Store or Google Play (an outage or an unexpected store response). Transient. Ask the user to try again later. Check store status pages.
PurchaseNotAllowedError The device is not allowed to make purchases (for example parental controls or restrictions). Tell the user to check device purchase restrictions and store account settings.
PurchaseInvalidError The purchase was invalid, often due to a payment problem on the store account. Ask the user to verify their payment method in their store account.
ProductNotAvailableForPurchaseError The product is not available for purchase right now. Confirm the product is approved and configured in the store and in RevenueCat.
ProductAlreadyPurchasedError The user already owns this product (common with non-consumables and active subscriptions). Call restorePurchases() to grant the existing entitlement.
ReceiptAlreadyInUseError The receipt is already associated with a different RevenueCat app user ID. Reconcile identity. Review your app user ID and login flow.
InvalidReceiptError The store receipt could not be validated. See the invalid receipt guide for sandbox and validation fixes.
MissingReceiptFileError No receipt file was found on the device (common in iOS sandbox). Have the user sign in to a sandbox account, then retry the purchase or restore.
NetworkError The SDK could not reach RevenueCat or the store servers. Transient. Retry with backoff. See the network error guide.
InvalidCredentialsError The API key is missing, wrong, or for the wrong platform. Developer bug. Use the correct platform-specific public API key in configure().
UnexpectedBackendResponseError RevenueCat returned a response the SDK could not parse. Usually transient. Retry. If persistent, check status and update the SDK.
InvalidAppUserIdError The app user ID is missing or malformed. Pass a stable, valid app user ID to logIn() or configure().
OperationAlreadyInProgressError The same operation (for example a purchase) is already running. Disable the buy button while a purchase is in flight. Do not call twice.
ConfigurationError Something in the RevenueCat or store configuration is wrong (for example products not set up). Developer bug. See the configuration error guide.
UnknownError An unexpected error that does not map to a known case. Log the underlying message, show a friendly generic message, and offer a retry.
Do not switch on raw numbers. Searches like "revenuecat error 23" usually mean a specific code surfaced in a log. Match on the named PurchasesErrorCode value instead of a numeric index, since the names are stable and self-documenting across platforms and SDK versions.

User Cancelled Is Not a Real Error

When a user opens the purchase sheet and then taps Cancel, the SDK reports it through the error channel, but it is not a failure you should surface. Showing an error alert here is a common bug that makes a normal flow feel broken. Always branch on the cancellation signal first:

  • React Native: check error.userCancelled (boolean); the code is PURCHASE_CANCELLED_ERROR.
  • Android: the purchaseWith onError callback gives you a userCancelled boolean; the code is PurchasesErrorCode.PurchaseCancelledError.
  • iOS: the error bridges to the .purchaseCancelledError case of RevenueCat.ErrorCode.
typescript
try {
  await Purchases.purchasePackage(pkg);
} catch (e: any) {
  if (e.userCancelled) return;   // do NOT show an error: the user simply cancelled
  // ...handle the real error by e.code
}

FAQ

What is PurchasesErrorCode in RevenueCat?
It is the typed enumeration RevenueCat surfaces on every failure. iOS bridges to RevenueCat.ErrorCode, Android exposes PurchasesErrorCode on a PurchasesError.code, and React Native gives a string error.code from the PURCHASES_ERROR_CODE enum. Switch on the code to respond to each case correctly.

How do I check if the user cancelled?
Check the cancellation flag first. React Native exposes error.userCancelled, Android passes a userCancelled boolean to the onError callback, and iOS bridges to the .purchaseCancelledError case. Return early and show nothing.

What does RevenueCat error 23 mean?
Match on the named PurchasesErrorCode value (for example ConfigurationError or NetworkError) rather than a raw number. RevenueCat also has separate backend error codes returned by the REST API. The official errors docs are the authoritative list.

How do I handle RevenueCat errors in code?
Use try/catch (React Native, Swift async) or the onError callback (Android), check the cancellation flag first, then switch on the code. Treat network errors as transient with a retry, treat configuration and credential errors as developer bugs to fix, and use a friendly generic message for UnknownError.