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 extendingError) with akinddiscriminant. Usee.kind === "..."to branch. - Swift — thrown as
RovenueError(a struct conforming toLocalizedError) with akind: ErrorKinddiscriminant. Catch withcatch let e as RovenueError. - Kotlin — thrown as
RovenueException(a plain class, not sealed) with akind: ErrorKinddiscriminant. There is no per-kind subclass and no exhaustivewhenover it — catchRovenueExceptionand branch one.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:
- Show a friendly message (e.g. "Your purchase is awaiting approval from your family organizer.").
- Do not grant entitlements — entitlements are only granted after the purchase completes asynchronously.
- 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'sENTITLEMENTS_CHANGEDvalue on each platform's change stream to know when the deferred purchase resolves: RNRovenue.addChangeListener((event) => { if (event === 'ENTITLEMENTS_CHANGED') { /* re-check entitlements */ } }); Swift/Kotlin — collectRovenue.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.sharedbeforeconfigure()triggers Swift'sfatalError, which aborts the process immediately. This is not a throwable Swift error: nodo/catchin Swift, notry/catchin JS, and notry/catchin 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 aRovenueException. Acatch (e: RovenueException)will not observe it at all; you'd need a broadercatch (e: Exception)to see it, and even then it carries noErrorKind— 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
IllegalStateExceptionisn't aRovenueException, it normalizes to the generickind: "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 fires | React Native (kind) | Swift (kind) | Kotlin (kind) |
|---|---|---|---|
| API key is missing, malformed, or rejected by the server | 'InvalidApiKey' | .invalidApiKey | ErrorKind.INVALID_API_KEY |
An argument was invalid — e.g. an empty string passed to identify() | 'InvalidArgument' | .invalidArgument | ErrorKind.INVALID_ARGUMENT |
| Server returned a non-recoverable error response | 'ServerError' | .serverError | ErrorKind.SERVER_ERROR |
| Device has no network connectivity | 'NetworkUnavailable' | .networkUnavailable | ErrorKind.NETWORK_UNAVAILABLE |
| Request exceeded the allowed time limit | 'Timeout' | .timeout | ErrorKind.TIMEOUT |
| Too many requests — SDK is being rate-limited; back off and retry | 'RateLimited' | .rateLimited | ErrorKind.RATE_LIMITED |
| Local persistent storage (MMKV) read/write failure | 'Storage' | .storage | ErrorKind.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' | .insufficientCredits | ErrorKind.INSUFFICIENT_CREDITS |
| Receipt could not be validated by the store or server | 'ReceiptInvalid' | .receiptInvalid | ErrorKind.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' | .subscriberUnresolvable | ErrorKind.SUBSCRIBER_UNRESOLVABLE |
| Unexpected internal SDK error | 'Internal' | .internal | ErrorKind.INTERNAL |
| User cancelled the store purchase sheet | 'PurchaseCanceled' | .purchaseCanceled | ErrorKind.PURCHASE_CANCELED |
| The selected product is not available in the user's storefront | 'ProductNotAvailable' | .productNotAvailable | ErrorKind.PRODUCT_NOT_AVAILABLE |
| The store returned a non-specific billing or connectivity error | 'StoreProblem' | .storeProblem | ErrorKind.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).
| Method | Possible kinds |
|---|---|
configure() | InvalidApiKey |
identify(appUserId) | InvalidArgument |
refreshEntitlements() | NetworkUnavailable, ServerError |
refreshVirtualCurrencies() | NetworkUnavailable, ServerError |
getOfferings() | NetworkUnavailable, ServerError |
purchase() | PurchaseCanceled, ProductNotAvailable, StoreProblem, ReceiptInvalid, NetworkUnavailable — plus a non-throwing isDeferred outcome, see Handling the Deferred purchase state |
restorePurchases() | NetworkUnavailable, ServerError |
setFallbackPlacements() | InvalidArgument |
refreshRemoteConfig() | NetworkUnavailable, Timeout, RateLimited, InvalidApiKey, ServerError |
setAttributes() — and setEmail(), setDisplayName(), setPhoneNumber(), setPushToken() | Storage |
flushAttributes() | NetworkUnavailable, Timeout, RateLimited, InvalidApiKey, Forbidden, ServerError |
getAppAccountToken() | Storage |
| All methods | Internal (unexpected failures) — see Calling the SDK before configure() for what happens if configure() was skipped; it is not one of these kinds |