Errors
Structured error codes and recovery guidance for the Kotlin SDK.
When an SDK call fails, it throws a VoidhashException. That includes Google Play Billing
failures: the SDK converts them, so a store problem never reaches your app as another exception
type, and code that catches VoidhashException (or any Exception) handles it. Every exception
carries a stable, machine-matchable code string. Match on error.code when the recovery
differs by cause, and report the full error for everything else.
This example handles one specific code and lets every other error propagate:
try {
val products = voidhash.getProducts()
} catch (error: VoidhashException) {
if (error.code == "FAILED_TO_GET_PRODUCTS") {
// retry, or render the screen without store metadata
}
}Most calls never throw because Voidhash is unreachable. Reads such as getCurrentPerson() and
getFeatureFlags() answer from cached state, flush() reports what is still queued, and
writes queue for later delivery. The codes below are the failures that do reach your code. The
message text is informational. Never match on it.
Store codes
| Code | Thrown by | Recovery guidance |
|---|---|---|
FAILED_TO_GET_PRODUCTS | getProducts() | Google Play did not return product details, for example while the device is offline. Retry, or render the screen without store metadata. |
BILLING_CONNECTION_FAILED | syncPurchases(), restorePurchases() | The Play Billing connection did not open: Google Play services are missing or updating, Play Billing is unavailable for the signed-in account, or setup did not finish in time. A later call retries it. |
STORE_UNAVAILABLE | syncPurchases(), restorePurchases() | Google Play could not list the purchases it owns. Retry later. |
RECONCILE_TRANSACTIONS_FAILED | restorePurchases(), syncPurchases() | See below. |
restorePurchases() throws RECONCILE_TRANSACTIONS_FAILED when at least one restorable receipt
was not accepted. That includes a receipt still queued because Voidhash could not be reached;
the SDK keeps it and delivers it in the background. It also includes a receipt the backend
refused, for example because Google Play is not configured for your app's package name. Prompt
the customer to try the restore again later. syncPurchases() throws it only when a receipt
could not be processed at all.
Reporting codes
| Code | Thrown by | Recovery guidance |
|---|---|---|
INVALID_TRANSACTION | reportTransaction(...) | Supply a non-empty Google Play purchase token. Multi-product purchases are unsupported. |
TRANSACTION_NOT_DURABLE | reportTransaction(...) | The device could not store the receipt, for example because its disk is full. Delivery is still attempted, but the receipt would not survive the app closing. Delay consuming the purchase and report it again later. |
Purchase codes
| Code | Thrown by | Recovery guidance |
|---|---|---|
READ_ONLY_PURCHASE_NOT_ALLOWED | purchase(...) | Purchase initiation is unavailable in the observer-only release. |
CONFIGURATION_MISSING | purchase(...) | The SDK is disabled, or no project schema has loaded yet. Retry once one has. |
USER_CANCELLED, PURCHASE_PENDING, PURCHASE_FAILED and BILLING_ERROR are reserved for
SDK-started purchases in the later commerce launch.
Argument codes
| Code | Thrown by | Recovery guidance |
|---|---|---|
INVALID_ARGUMENT | identify(...), setPersonAttributes(...) | A distinct ID was empty or only whitespace, longer than 255 characters, contained a NUL character or started with vh:anon:, or an attribute value was a map or a list. Fix the call; nothing was sent or queued. |
Lifecycle codes
| Code | Thrown by | Recovery guidance |
|---|---|---|
CLIENT_SHUT_DOWN | identify(...), setPersonAttributes(...), reset(), getProducts(), purchase(...), restorePurchases(), syncPurchases() | The client was shut down, or replaced by a later configure(...). Use the client configure(...) returned last, or Voidhash.shared. |
API codes
A write the API refuses outright throws a VoidhashApiException. It is a VoidhashException
whose status and tag describe the API's answer. Branch on code and tag rather than on the
status. identify(...) throws one in these cases and restores the previous identity:
| Status | Tag | Meaning |
|---|---|---|
400 | Api/SdkValidationError | The request is invalid. |
404 | Api/SdkPersonNotFoundError | Voidhash does not know the target. |
A 409 (Api/SdkPersonAlreadyIdentifiedError) means the anonymous identity is already linked to
another account. identify(...) does not throw for it: the device switches to the customer you
identified without merging the anonymous identity, and the SDK reports PERSON_WRITE_REJECTED.
setPersonAttributes(...) throws for a payload the API refuses, such as a 400. Its "no such
person" answer is not a refusal: the attributes stay queued until the person exists.
Writes that fail because Voidhash is unreachable, because an answer could not be read, or because
the publishable key was rejected are queued instead and report WriteStatus.DEFERRED.
INVALID_REQUEST is thrown before anything is sent, when the SDK cannot build the request at all.
| Code | Meaning |
|---|---|
API_ERROR | The API refused the request. Check status and tag; sending it again gets the same answer. |
AUTHENTICATION_FAILED | The publishable key was rejected. Outbound traffic pauses, queued data is kept, and writes report DEFERRED instead. |
INVALID_REQUEST | No request could be built: check the base URL and publishable key. |
Diagnostics
Failures the SDK handles on its own never throw. They are reported through the onDiagnostic
option instead, as a VoidhashDiagnostic with a kind, a stable code, the operation, and
whether the SDK retries. The handler runs on a background thread, one report at a time in the
order they were made, and an exception it throws is ignored. Messages never contain a user ID or
a purchase token.
VoidhashOptions(
onDiagnostic = { diagnostic ->
Log.d("Voidhash", "${diagnostic.code}: ${diagnostic.message}")
},
)| Code | Meaning |
|---|---|
AUTHENTICATION_FAILED | The publishable key was rejected. Outbound traffic pauses and queued data is kept. Reported once per pause; AUTHENTICATION_RECOVERED follows. |
CIRCUIT_OPEN | Voidhash failed five times in a row, so requests are skipped and cached data is served until a retry succeeds. CIRCUIT_CLOSED reports the recovery. |
TRANSACTION_SYNC_DEFERRED | A receipt is still queued after a delivery attempt that failed or was not confirmed. It is retried. |
TRANSACTION_REJECTED | Voidhash refused a receipt, for example because Google Play is not configured for the package. It is sent again on the next launch. |
PERSON_WRITE_REJECTED | A queued identify or attribute update was dropped because the API refused it, or a sign-in could not merge an anonymous identity another account owns. |
ANALYTICS_SEND_FAILED | Analytics events were not delivered and are retried. |
ANALYTICS_EVENT_DROPPED | An event was dropped at the 1,000-event queue limit, or refused by the API. Only the refused event is dropped, never the rest of its batch. |
ANALYTICS_EVENT_INVALID | An event was not captured because its name was blank, or because there was no distinct ID to attribute it to. |
SCREEN_MAPPER_FAILED | Your mapScreen function threw, so that screen was not captured. The exception does not reach your app. |
CACHE_READ_FAILED | An unreadable cache entry was discarded. |
CACHE_WRITE_FAILED | The device could not store a value. It is kept in memory and stored with the next write that succeeds. |
TRANSACTION_OUTBOX_BACKLOG | More than 1,000 receipts are waiting for delivery. None was dropped. |
The package README lists every code.