Errors
Error codes and diagnostics of the React Native SDK, with what to do for each.
The React Native SDK reports failures as errors with a stable code, named after the operation
that failed; the native iOS and Android SDK's message is kept in message.
Handle errors
Client methods never throw or reject. They resolve to a better-result
Result; hooks expose the last error in error. Check isErr(), then branch on error.code.
The message is for people; never match on it.
const result = await voidhash.client.purchase(monthly, { offerId: "winback_50" });
if (result.isErr()) {
if (result.error.code === "OFFER_UNAVAILABLE") showRegularPrice();
else reportError(result.error);
} else if (result.value.status === "completed") {
showThankYou();
}Cancelled, pending and deferred purchases aren't errors: they are Ok outcomes of purchase().
See Start a purchase. Network failures rarely
surface as errors: reads answer from the cache and writes stay queued, reported as
diagnostics.
Error types
The error class isn't exported, so compare error.code with the strings below. Two types are:
| Type | Use |
|---|---|
FlowErrorCode | Union of the FLOW_* codes in a failed presentFlow() result. |
FlowResult | The result of presentFlow(), including { status: "failed", error }. |
Error codes
Configuration
| Code | When | What to do |
|---|---|---|
FAILED_TO_INITIALIZE_VOIDHASH_CLIENT | init() failed, or the provider's initError: the binary lacks the native modules, or the native client didn't start. | Rebuild the app; Expo Go isn't supported. Offer retryInit. |
VOIDHASH_CLIENT_NOT_INITIALIZED | A read ran before initialization finished or after end() was called, or initialization failed. Identity, restore, sync and report calls wait on their own. | Wait for useVoidhash().status === "ready". |
FAILED_TO_END_VOIDHASH_CLIENT | end() failed. | Log it. |
INVALID_ARGUMENT | In development builds, createVoidhashClient got a baseUrl or ingestUrl that isn't an absolute http(s) URL, or an unknown analyticsConsent. | Fix the option. Release builds use the default and report INVALID_CONFIGURATION. |
READ_ONLY_PURCHASE_NOT_ALLOWED | purchase() or setPersonAttributesSync() ran in observer mode. | Buy through your other billing SDK. |
UNKNOWN | A hook hit an unexpected failure. | Report the full error. |
Products and purchases
| Code | When | What to do |
|---|---|---|
FAILED_TO_GET_PRODUCTS | getProducts() or useProducts() couldn't read the store. | Retry, or render without store prices. |
FAILED_TO_PURCHASE | purchase() failed; nothing was bought. The native code is in the message, such as PURCHASE_FAILED, or on Android ITEM_ALREADY_OWNED and STORE_UNAVAILABLE (Google Play couldn't be reached). | Let the user try again. For ITEM_ALREADY_OWNED, offer to restore. |
OFFER_UNAVAILABLE | purchase() with an offerId that can't be bought now. Nothing was bought. | Offer the regular product. |
REPLACED_SUBSCRIPTION_NOT_OWNED | Android. purchase() with replacing named a subscription the Google Play account doesn't own. Nothing was bought. | Buy without replacing, or name the subscription the person has. |
SUBSCRIPTION_REPLACEMENT_REFUSED | Android. Google Play refused the replacement purchase() asked for before its sheet opened; its reason is in the message. Nothing was bought. | Read the message; another mode may be accepted. |
INVALID_ARGUMENT | purchase() got a replacing that names no product or an unknown mode, or on Android one that isn't a different subscription. Nothing was bought. | Fix the call. |
FAILED_TO_RESTORE_PURCHASES | restorePurchases() couldn't reach the store, or a receipt isn't confirmed yet. | Ask the user to retry. Queued receipts stay queued. |
FAILED_TO_SYNC_PURCHASES | syncPurchases() couldn't read the store. | Retry. |
FAILED_TO_REPORT_TRANSACTION | reportTransaction() got an invalid input. | Pass an App Store transaction ID or a Google Play purchase token. |
FAILED_TO_SHOW_MANAGE_SUBSCRIPTIONS | iosShowManageSubscriptions() couldn't open the sheet. iOS only. | Link to the App Store's subscription settings instead. |
FAILED_TO_PRESENT_CODE_REDEMPTION_SHEET | iosPresentCodeRedemptionSheet() couldn't open the sheet. iOS only. | Retry from a visible screen. |
Paywalls and flows
presentFlow() resolves to { status: "failed", error } for the FLOW_* codes; it never rejects.
| Code | When | What to do |
|---|---|---|
FLOW_NOT_ASSIGNED | Nothing is assigned to the location, it doesn't exist or is archived, or the build needs a newer SDK. | Show your fallback screen. |
FLOW_UNAVAILABLE | Nothing is cached for the location and Voidhash is unreachable, the download failed, or the paywall wasn't ready to show within 15 seconds. | Show your fallback; retry later. |
FLOW_RENDERER_MISSING | The build doesn't include the paywall renderer. | Add the renderer and rebuild. |
FLOW_UNSUPPORTED_PACKAGE | The paywall was built in a format this SDK can't read. | Update the SDK. |
FLOW_PRESENTATION_FAILED | There was nothing to present from, or the renderer failed. | Present from a visible screen; report the error. |
FLOW_DISABLED | The client was created with enabled: false. | Nothing. |
FAILED_TO_GET_PAYWALL_FOR_LOCATION | getPaywallForLocation() failed, or the paywall couldn't be read. | Show your own upgrade screen. |
PAYWALL_UNAVAILABLE | getPaywallForLocation() found nothing cached and Voidhash is unreachable. | Hide the paywall; retry once online. |
See Fall back when nothing was shown.
Identity
| Code | When | What to do |
|---|---|---|
INVALID_ARGUMENT | identify() got a blank user ID, a person attribute was a nested object or array, or setAnalyticsConsent() got an unknown value. Nothing changed. | Fix the call. |
FAILED_TO_IDENTIFY | identify() or identifySync(): Voidhash refused the sign-in for good, and the previous identity was kept. | Check the user ID. |
FAILED_TO_SIGN_OUT, FAILED_TO_RESET | signOut() or reset() failed. | Retry. |
FAILED_TO_SET_GROUP | group() failed on the device. A group type or key Voidhash can't keep is INVALID_ARGUMENT instead. | Retry. |
FAILED_TO_RESET_GROUPS | resetGroups() failed on the device. | Retry. |
FAILED_TO_GET_CURRENT_PERSON | getCurrentPerson() or hasPerk() failed. | Show cached state and retry. |
FAILED_TO_GET_DISTINCT_ID | getDistinctId() failed. | Retry after initialization. |
FAILED_TO_SET_PERSON_ATTRIBUTES | setPersonAttributes() failed. | Retry later. Updates already stored are sent on their own. |
FAILED_TO_SET_PERSON_ATTRIBUTES_SYNC | setPersonAttributesSync() failed, or Voidhash refused the write. | Retry. |
FAILED_TO_RESET_PERSON_CACHE | resetCache() failed. | Safe to ignore. |
FAILED_TO_GET_FEATURE_FLAGS | getFeatureFlags() failed. | Treat flags as off. |
FAILED_TO_FLUSH_ANALYTICS | flush() couldn't send events now; it's the result's lastError. | Nothing. Events stay queued. |
FAILED_TO_SET_ANALYTICS_CONSENT | The consent applies for this run but wasn't stored. | Set it again on next launch. |
Network and API
| Code | When | What to do |
|---|---|---|
AUTHENTICATION_FAILED | The publishable key was rejected and nothing was cached. getCurrentPerson(), getFeatureFlags() and getPaywallForLocation() return it once; later reads answer from the cache. | Check the key and its project. See API keys. |
Other network failures don't produce errors. identifySync() and setPersonAttributesSync()
report an unreachable Voidhash as { status: "deferred" }. A 401 or 403 from something in
front of Voidhash, such as a proxy, a firewall or an edge page, isn't a rejected key: the SDK
retries it like any temporary failure, and nothing pauses.
Diagnostics
onDiagnostic reports problems the SDK already recovered from; see
Diagnostics for the handler and fields. The native SDKs'
diagnostics arrive unchanged: see the Swift and
Kotlin lists. The ones worth sending to your monitoring, and the
ones only React Native reports:
| Code | Meaning |
|---|---|
AUTHENTICATION_FAILED | Voidhash itself rejected the publishable key. The refused traffic pauses and stays queued; fixing the key and relaunching delivers it. |
CIRCUIT_OPEN | Voidhash failed 5 times in a row; requests are skipped and the cache is served until the next probe. |
TRANSACTION_REJECTED | Voidhash refused a store receipt, for example because the store isn't set up for this app. It's sent again at the next launch, or after 6 hours. |
TRANSACTION_SYNC_DEFERRED | A receipt is still queued after a failed sync. |
PURCHASE_FINISHED_BEFORE_SYNC | Android only. Voidhash hadn't recorded a purchase 36 hours after payment, so the SDK acknowledged it to stop Google Play's refund. |
PERSON_WRITE_REJECTED | A queued identify() or attribute update was refused for good. |
ANALYTICS_EVENT_DROPPED | Events were dropped at the 1,000-event queue limit, refused by Voidhash, or had properties that couldn't be copied; or a screen couldn't be tracked, such as one whose name isn't a string. |
ANALYTICS_CONSENT_DENIED | Analytics weren't collected because consent isn't granted. |
INVALID_CONFIGURATION | A release build ignored an invalid baseUrl, ingestUrl or analyticsConsent and used the default. |
HOST_CALLBACK_FAILED | A callback you passed threw, such as a usePurchase callback or screenTracking.mapScreen, or mapScreen returned a screen whose identity, name or path isn't a string. The SDK carried on without it. |
BACKGROUND_TASK_FAILED | A background task, such as a refresh or a queue sync, failed outright. |
LEGACY_IMPORT_SKIPPED | iOS only. Data from an earlier release wasn't carried over because it was stored for another base URL. |
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.