Rovenue SDK
Reference

Errors

The RovenueError family and how each surfaces per platform.

Every Rovenue SDK error originates from the same Rust-core RovenueError type and is mapped to an idiomatic type on each platform:

  • React Native — thrown as RovenueError (a class extending Error) with a kind discriminant. Use e.kind === "..." to branch.
  • Swift — thrown as RovenueError (a struct conforming to LocalizedError) with a kind: ErrorKind discriminant. Catch with catch let e as RovenueError.
  • Kotlin — thrown as RovenueException (a plain class, not sealed) with a kind: ErrorKind discriminant. There is no per-kind subclass and no exhaustive when over it — catch RovenueException and branch on e.kind.

Calling the HTTP API directly instead of through an SDK? See API Errors for the { error: { code, message } } envelope's code values — a different, larger set than the kinds below.


Migrating from 0.15.x → 0.16.0

Breaking change in 0.16.0 — the per-type error subclasses and Swift Rovenue.Error enum cases are replaced by a single RovenueError/RovenueException with a kind string discriminant, and a pending (Ask-to-Buy) purchase is now a non-throwing isDeferred outcome rather than a thrown error.

React Native: instanceof → e.kind

// Before (0.15.x)
import { InsufficientCreditsError, NetworkUnavailableError } from '@rovenue/react-native-sdk';
try {
  await Rovenue.purchase(pkg);
} catch (e) {
  if (e instanceof InsufficientCreditsError) { /* … */ }
  if (e instanceof NetworkUnavailableError) { /* … */ }
}

// After (0.16.0)
import { RovenueError } from '@rovenue/react-native-sdk';
try {
  const result = await Rovenue.purchase(pkg);
  if (result.isDeferred) {
    // Ask-to-Buy / parental controls — not an error. Wait for
    // Rovenue.addChangeListener to report 'ENTITLEMENTS_CHANGED' before
    // granting entitlements (see "Handling the Deferred purchase state" below).
    showToast('Your purchase is awaiting approval.');
    return;
  }
} catch (e) {
  if (e instanceof RovenueError) {
    if (e.kind === 'InsufficientCredits') { /* … */ }
    if (e.kind === 'NetworkUnavailable') { /* … */ }
  }
}

Swift: Rovenue.Error enum → RovenueError struct

// Before (0.15.x)
do {
  try await Rovenue.shared.purchase(pkg)
} catch Rovenue.Error.purchasePending { showToast("Awaiting approval.") }
  catch Rovenue.Error.insufficientCredits { /* … */ }

// After (0.16.0)
do {
  let result = try await Rovenue.shared.purchase(pkg)
  if result.isDeferred { showToast("Awaiting approval."); return }
} catch let e as RovenueError {
  switch e.kind {
  case .insufficientCredits: /* … */
  default: throw e
  }
}

Kotlin: standalone exceptions → RovenueException.kind

// Before (0.15.x)
try {
  Rovenue.shared.purchase(activity, pkg)
} catch (e: PurchasePendingException) { showToast("Awaiting approval.") }
  catch (e: PurchaseCancelledException) { /* user cancelled */ }

// After (0.16.0)
val outcome = Rovenue.shared.purchase(activity, pkg)
if (outcome.isDeferred) { showToast("Awaiting approval."); return }
// Errors:
try {
  Rovenue.shared.purchase(activity, pkg)
} catch (e: RovenueException) {
  when (e.kind) {
    ErrorKind.PURCHASE_CANCELED -> { /* user cancelled — no action */ }
    else -> throw e
  }
}

Handling the Deferred purchase state

purchase() no longer throws for Ask-to-Buy / parental-control holds. Instead, it returns a PurchaseResult (the same return type as a normal successful purchase) with isDeferred = true. You should:

  1. Show a friendly message (e.g. "Your purchase is awaiting approval from your family organizer.").
  2. Do not grant entitlements — entitlements are only granted after the purchase completes asynchronously.
  3. There is no purchase-specific notification — the SDK reconciles the pending transaction in the background and reports the result the same way as any other entitlement change. Listen for ChangeEvent's ENTITLEMENTS_CHANGED value on each platform's change stream to know when the deferred purchase resolves: RN Rovenue.addChangeListener((event) => { if (event === 'ENTITLEMENTS_CHANGED') { /* re-check entitlements */ } }); Swift/Kotlin — collect Rovenue.shared.changes (AsyncStream<ChangeEvent> / SharedFlow<ChangeEvent>) and match .entitlementsChanged / ENTITLEMENTS_CHANGED.


Calling the SDK before configure()

This is not an entry in the table below — it isn't part of the ErrorKind taxonomy at all, and it is not something to write a catch for. Calling any other SDK method before configure() has run is a programming error to fix at the call site (always configure() first, at app startup — see App lifecycle), not a runtime condition your code should branch on. What actually happens if you get the ordering wrong, per platform:

  • iOS (native Swift, and React Native / Flutter on iOS, which call into the same Swift singleton) — a hard crash. Accessing Rovenue.shared before configure() triggers Swift's fatalError, which aborts the process immediately. This is not a throwable Swift error: no do/catch in Swift, no try/catch in JS, and no try/catch in Dart can intercept it — the crash log will show "Rovenue: must call Rovenue.configure(apiKey:baseUrl:) before accessing shared" just before the process terminates.
  • Android (native Kotlin) — throws a plain IllegalStateException, not a RovenueException. A catch (e: RovenueException) will not observe it at all; you'd need a broader catch (e: Exception) to see it, and even then it carries no ErrorKind — it's outside the taxonomy this page documents.
  • React Native on Android and Flutter on Android — the native bridge on this platform catches everything and re-surfaces it as a normal, catchable error, but because the underlying IllegalStateException isn't a RovenueException, it normalizes to the generic kind: "Internal" (RN) / RovenueErrorKind.internal (Flutter) — indistinguishable from any other unexpected internal failure, not a signal you can branch on to detect "not configured" specifically.

The only reliable fix is ordering: call configure() before anything else, every time.

Error table

Every row names the actual value to check — there is no per-kind subclass or nested type on any platform (see the platform notes at the top of this page). React Native and Swift check RovenueError.kind; Kotlin checks RovenueException.kind.

When it firesReact Native (kind)Swift (kind)Kotlin (kind)
API key is missing, malformed, or rejected by the server'InvalidApiKey'.invalidApiKeyErrorKind.INVALID_API_KEY
An argument was invalid — e.g. an empty string passed to identify()'InvalidArgument'.invalidArgumentErrorKind.INVALID_ARGUMENT
Server returned a non-recoverable error response'ServerError'.serverErrorErrorKind.SERVER_ERROR
Device has no network connectivity'NetworkUnavailable'.networkUnavailableErrorKind.NETWORK_UNAVAILABLE
Request exceeded the allowed time limit'Timeout'.timeoutErrorKind.TIMEOUT
Too many requests — SDK is being rate-limited; back off and retry'RateLimited'.rateLimitedErrorKind.RATE_LIMITED
Local persistent storage (MMKV) read/write failure'Storage'.storageErrorKind.STORAGE
Returned by the server when a virtual-currency spend transaction has insufficient balance (server-side API only — no client SDK method throws this)'InsufficientCredits'.insufficientCreditsErrorKind.INSUFFICIENT_CREDITS
Receipt could not be validated by the store or server'ReceiptInvalid'.receiptInvalidErrorKind.RECEIPT_INVALID
The server cannot resolve this device's subscriber because its merge history is corrupt (SUBSCRIBER_UNRESOLVABLE). Thrown by identify(); not retryable — the project owner must repair the data. Queued events and attributes are kept and delivered after the repair'SubscriberUnresolvable'.subscriberUnresolvableErrorKind.SUBSCRIBER_UNRESOLVABLE
Unexpected internal SDK error'Internal'.internalErrorKind.INTERNAL
User cancelled the store purchase sheet'PurchaseCanceled'.purchaseCanceledErrorKind.PURCHASE_CANCELED
The selected product is not available in the user's storefront'ProductNotAvailable'.productNotAvailableErrorKind.PRODUCT_NOT_AVAILABLE
The store returned a non-specific billing or connectivity error'StoreProblem'.storeProblemErrorKind.STORE_PROBLEM

There is no PurchasePending (or equivalent) row: as covered above, Ask-to-Buy / parental-control holds are not an error on any platform as of 0.16.0 — purchase() returns a PurchaseResult with isDeferred = true instead of throwing. See Handling the Deferred purchase state.

InvalidApiKey vs InvalidArgument

InvalidApiKey fires when the SDK detects that your API key is malformed (wrong prefix) at configure() time, or when the server rejects it on the first network request. It indicates a configuration problem — check your dashboard key.

InvalidArgument fires when your code passes an invalid value to a method at call time — for example, an empty string to identify(). It indicates a programming error in your call site.


Handling errors

All throwing SDK methods should be wrapped in try/catch. The recommended pattern is to handle the errors you expect explicitly, then re-throw anything unexpected so it surfaces in your crash reporting.

import { Rovenue, RovenueError } from '@rovenue/react-native-sdk';

// --- Purchase flow ---
try {
  const result = await Rovenue.purchase(pkg);
  // success — check result.isDeferred for Ask-to-Buy (see below;
  // it does not throw)
} catch (e) {
  if (!(e instanceof RovenueError)) throw e; // not an SDK error — re-throw
  switch (e.kind) {
    case 'PurchaseCanceled':
      // User dismissed the store sheet — no action needed
      break;
    case 'ProductNotAvailable':
      showToast('This product is not available in your region.');
      break;
    case 'ReceiptInvalid':
      showRetryPrompt();
      break;
    case 'NetworkUnavailable':
      showToast('No internet connection. Please try again.');
      break;
    default:
      throw e; // re-throw unexpected SDK errors
  }
}
import Rovenue

// --- Purchase flow ---
do {
  let result = try await Rovenue.shared.purchase(pkg)
  // success — check result.isDeferred for Ask-to-Buy (see below;
  // it does not throw)
} catch let e as RovenueError {
  switch e.kind {
  case .purchaseCanceled:
    break // User dismissed the store sheet — no action needed
  case .productNotAvailable:
    showToast("This product is not available in your region.")
  case .receiptInvalid:
    showRetryPrompt()
  case .networkUnavailable:
    showToast("No internet connection. Please try again.")
  default:
    throw e // re-throw unexpected SDK errors
  }
} catch {
  throw error // not a RovenueError — re-throw
}
import dev.rovenue.sdk.Rovenue
import dev.rovenue.sdk.RovenueException
import dev.rovenue.sdk.generated.ErrorKind

// --- Purchase flow ---
try {
    // `activity` is your current Activity — Play Billing requires it.
    val result = Rovenue.shared.purchase(activity, pkg)
    // success — check result.isDeferred for Ask-to-Buy (see below;
    // it does not throw)
} catch (e: RovenueException) {
    when (e.kind) {
        ErrorKind.PURCHASE_CANCELED -> {
            // User dismissed the store sheet — no action needed
        }
        ErrorKind.PRODUCT_NOT_AVAILABLE -> {
            showToast("This product is not available in your region.")
        }
        ErrorKind.RECEIPT_INVALID -> {
            showRetryPrompt()
        }
        ErrorKind.NETWORK_UNAVAILABLE -> {
            showToast("No internet connection. Please try again.")
        }
        else -> throw e // re-throw unexpected SDK errors
    }
}

A PurchaseCanceled kind is not an error from the user's perspective. It fires whenever the store sheet is dismissed without a completed payment — including when the user deliberately taps Cancel. Do not show an error dialog for this case.


Which methods throw which errors

Values below are kinds (see the error table above for the exact per-platform spelling of each).

On this page