Errors
Exception types, error codes and diagnostics of the Kotlin SDK, with what to do for each.
The Kotlin SDK throws a VoidhashException with a stable code: Google Play Billing failures are
mapped to the SDK's own codes, and Voidhash API failures are mapped from the HTTP status.
Handle errors
Suspending methods throw on failure. Rethrow CancellationException so coroutine cancellation
keeps working, then branch on code. The message is for people; never match on it.
import com.voidhash.sdk.VoidhashException
import kotlinx.coroutines.CancellationException
lifecycleScope.launch {
try {
voidhash.purchase(activity, monthly)
} catch (error: CancellationException) {
throw error
} catch (error: VoidhashException) {
when (error.code) {
"USER_CANCELLED", "PURCHASE_PENDING" -> Unit // Expected outcomes, not failures.
"OFFER_UNAVAILABLE" -> showRegularPrice()
else -> showPurchaseError(error)
}
}
}Most calls don't throw when Voidhash is unreachable: reads answer from the cache, writes are queued,
and flush() reports what is still pending. Failures the SDK handles itself are
diagnostics.
Error types
| Type | Code | Thrown for |
|---|---|---|
VoidhashException | Any | Base type of every SDK error. Has code and description. |
VoidhashApiException | API_ERROR, AUTHENTICATION_FAILED, RATE_LIMIT_EXCEEDED, MALFORMED_RESPONSE | Voidhash answered with an error. Adds status, tag and retryAfterMs. |
VoidhashNetworkException | NETWORK_ERROR | A request never reached Voidhash. Usually handled by the SDK; it can appear as another error's cause. |
FlowException | FLOW_<reason> | Carried by FlowResult.Failed, never thrown. Adds reason and abiVersion. |
Error codes
Configuration
| Code | When | What to do |
|---|---|---|
CONFIGURATION_MISSING | The client is disabled, or the project configuration hasn't loaded yet. | Retry after initialize(). |
CLIENT_SHUT_DOWN | The client was shut down or replaced by a later configure. | Use Voidhash.shared. |
READ_ONLY_PURCHASE_NOT_ALLOWED | purchase ran in observer mode. | Buy through your other billing SDK. |
PRODUCT_NOT_FOUND | In development mode, the product has no development data. | Give the product a billing duration. |
DEVICE_LOCKED | A call ran before the device was first unlocked after a restart, and couldn't be held until then. See Create the client. | Call again once the device is unlocked. |
AUTHENTICATION_FAILED, a rejected publishable key, is listed under Network and API.
Products and purchases
| Code | When | What to do |
|---|---|---|
USER_CANCELLED | The person closed the Google Play sheet. | Nothing. |
PURCHASE_PENDING | The payment awaits approval. | Nothing. When Google Play completes it, the SDK sends and acknowledges it, even after a relaunch. |
OFFER_UNAVAILABLE | Google Play doesn't offer this offerId to the person, or the mapping has no such offer. Nothing was bought. | Offer the regular price. |
PURCHASE_FAILED | Google Play returned no purchase, or the subscription has no base plan offer. | Check the product's Google Play mapping. |
PURCHASE_UNKNOWN_RESULT | Google Play returned a purchase in an unknown state. | Report the error. |
PURCHASE_IN_PROGRESS | Another Google Play purchase sheet is already open. | Wait for it to close. |
PURCHASE_EMPTY | Google Play reported success but returned no purchase. | Report the error; refresh the person before retrying. |
REPLACED_SUBSCRIPTION_NOT_OWNED | 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. See Change a subscription. |
SUBSCRIPTION_REPLACEMENT_REFUSED | Google Play refused the replacement before its sheet opened. Nothing was bought. | Report the error; the message carries Google Play's reason. |
SKU_NOT_FOUND | Google Play doesn't sell the product, or its details weren't loaded. | Check the product ID in the Play Console and the mapping. |
ITEM_ALREADY_OWNED | The person already owns the product, so Google Play won't sell it again. | Offer to restore. The SDK delivers and finishes what it owns of the product, so a consumable can be bought again. |
BILLING_ERROR | Google Play reported any other failure while buying. | Report the error. |
GOOGLE_BILLING_NOT_INITIALIZED | Google Play services are missing, or Play Billing setup didn't finish. | Let the person retry later. |
CURRENT_ACTIVITY_NULL | No activity was available to launch the sheet from. | Pass a visible activity. |
CONNECTION_ENDED | The billing connection closed during the purchase, for example on shutdown. | Let the person retry. |
FAILED_TO_GET_PRODUCTS | getProducts() got no product details, for example offline. | Retry, or render without prices. |
BILLING_CONNECTION_FAILED | restorePurchases() or syncPurchases() couldn't connect to Play Billing. | A later call retries. |
STORE_UNAVAILABLE | purchase couldn't reach Google Play, or restorePurchases() or syncPurchases() couldn't list owned purchases. | Retry later. If the person completed the purchase anyway, the SDK finds it and finishes it. |
RECONCILE_TRANSACTIONS_FAILED | A restore or sync couldn't deliver a purchase to Voidhash. | Ask the person to retry later. The SDK keeps the purchase and retries. |
INVALID_TRANSACTION | reportTransaction got a blank purchase token or a multi-product purchase. | Pass a single-product purchase. |
TRANSACTION_NOT_DURABLE | The device couldn't store a reported purchase, for example with a full disk or before its first unlock. | Don't consume the purchase yet; report it again. |
syncPurchases() throws RECONCILE_TRANSACTIONS_FAILED only when a purchase couldn't be processed
at all; restorePurchases() also throws it while a purchase is still queued, or when Voidhash
hasn't answered within 10 seconds. The restore then continues in the background.
When Google Play reported the failure, the exception's cause is a BillingEngineException whose
responseCode is Google Play's own response code.
Paywalls and flows
presentFlow never throws. It returns FlowResult.Failed, whose error.reason names the cause.
| 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 and Voidhash is unreachable, the paywall couldn't be downloaded, or it wasn't ready to show within 15 seconds. | Show your fallback; retry later. |
FLOW_RENDERER_MISSING | paywallRenderer isn't set. | Add the renderer. |
FLOW_UNSUPPORTED_PACKAGE | The paywall was built in a format this SDK can't read. | Update the SDK. |
FLOW_PRESENTATION_FAILED | The paywall couldn't be shown, or three frames in a row failed to draw while it was on screen. | Present from a visible activity; report the error. |
FLOW_DISABLED | The client is disabled or shut down. | Nothing. |
See Handle failures.
Identity
| Code | When | What to do |
|---|---|---|
INVALID_ARGUMENT | identify got a user ID that is blank, longer than 255 characters, contains NUL, or starts with vh_anon_ or vh:anon:. Or setPersonAttributes got a map or list value. Nothing was sent. | Fix the call. See Identify users. |
identify throws a VoidhashApiException with code API_ERROR when Voidhash refuses the sign-in,
and restores the previous identity. Its tag is Api/SdkValidationError (400) or
Api/SdkPersonNotFoundError (404). A 409 doesn't throw. Writes that fail because Voidhash is
unreachable, unreadable, rejects the key, or takes longer than 2 seconds are queued and return
WriteStatus.DEFERRED.
Network and API
| Code | When | What to do |
|---|---|---|
AUTHENTICATION_FAILED | Voidhash itself answered 401 or 403: the publishable key was rejected. getCurrentPerson and getFeatureFlags throw it once, when nothing is cached. | Check the key and its project. See API keys. |
RATE_LIMIT_EXCEEDED | Voidhash answered 429. | Wait retryAfterMs; don't retry in a loop. |
API_ERROR | Any other error status. | Branch on tag, not on status; report the rest. |
MALFORMED_RESPONSE | Voidhash's answer couldn't be read. | Report the error. |
NETWORK_ERROR | The request never reached Voidhash. | Retry later. |
INVALID_REQUEST | No request could be built. | Check baseUrl and the publishable key. |
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 treats it as a temporary failure: it retries, and nothing pauses.
Diagnostics
Failures the SDK handles itself never throw. Receive them with the onDiagnostic option as a
VoidhashDiagnostic with a kind, code, operation, message, and whether the SDK retries. The
handler runs on one background thread, in order; exceptions it throws are ignored. Messages never
contain a user ID or purchase token.
VoidhashOptions(
onDiagnostic = { diagnostic ->
Log.d("Voidhash", "${diagnostic.code}: ${diagnostic.message}")
},
)Diagnostic codes lists every code and what it means.
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.