Errors
Error types, codes and diagnostics of the Swift SDK, with what to do for each.
The Swift SDK throws errors with a stable code: StoreKit failures are wrapped in the SDK's own
codes, and Voidhash API failures are mapped from the HTTP status.
Handle errors
Methods that can fail are async throws. Catch the error type, then branch on code. The error's
string form is "CODE: message"; the message is for people, so never match on it.
import Voidhash
import VoidhashCore
func buy(_ product: VoidhashProduct) async {
guard let voidhash = Voidhash.shared else { return }
do {
// A cancelled sheet and a pending purchase are results, not errors.
_ = try await voidhash.purchase(product, options: VoidhashPurchaseOptions(offer: .id("winback_50")))
} catch let error as VoidhashStoreError {
switch error.code {
case "OFFER_UNAVAILABLE":
showRegularPrice()
default:
showPurchaseError(error)
}
} catch {
showPurchaseError(error)
}
}Most calls don't throw when Voidhash is unreachable: reads answer from the cache and writes are queued. Failures the SDK handles itself arrive as diagnostics instead.
Error types
| Type | Module | Thrown for |
|---|---|---|
VoidhashStoreError | VoidhashCore | Store operations, purchases and invalid arguments. Has code, message and, for a refused purchase, reason. |
VoidhashApiError | VoidhashCore | Requests Voidhash rejected or that never completed. Adds statusCode, tag and retryAfterMilliseconds. |
ReconcileTransactionsError | Voidhash | A restore whose receipts Voidhash didn't accept. Its code is RECONCILE_TRANSACTIONS_FAILED; adds transactionIds. |
FlowError | Voidhash | Not thrown: returned in .failed by presentFlow. Its description starts with a FLOW_ code. |
Error codes
Configuration
| Code | When | What to do |
|---|---|---|
STOREKIT_NOT_INITIALIZED | A store call ran on a disabled or shut-down client. | Configure the client with enabled on, and use Voidhash.shared. |
METHOD_NOT_AVAILABLE_PLATFORM | The method isn't available here, such as store sheets in development mode or on visionOS. | Call it from a store build on iOS. |
READ_ONLY_PURCHASE_NOT_ALLOWED | purchase ran in observer mode. | Buy through your other billing SDK. |
AUTHENTICATION_FAILED, a rejected publishable key, is listed under Network and API.
Products and purchases
purchase(_:options:) returns .cancelled and .pending as results rather than throwing them.
| Code | When | What to do |
|---|---|---|
USER_CANCELLED | The user dismissed the App Store sign-in during a restore, or StoreKit reported a cancel by throwing. | Nothing. |
PURCHASE_IN_PROGRESS | Another purchase is still open. Nothing was bought. | Wait for the first purchase to end. |
INVALID_ARGUMENT | A quantity below 1, or above 1 for a product that isn't a consumable. | Buy one, or only buy consumables in a quantity. |
INVALID_PRODUCT_ID | The product isn't loaded from the App Store. | Buy a product getProducts returned. |
OFFER_UNAVAILABLE | The offer can't be applied, for example because the user isn't eligible, or Voidhash couldn't be reached to sign the offer. Nothing was bought. When the App Store refused it as the user isn't eligible, reason is .ineligibleForOffer. | Load the products again and offer the regular price. |
PURCHASE_FAILED | StoreKit failed the purchase. | Let the user try again. |
PURCHASE_UNKNOWN_RESULT | StoreKit returned a purchase result this SDK version doesn't know. | Report the error; let the user try again. |
TRANSACTION_VERIFICATION_FAILED | StoreKit couldn't verify the transaction, or Voidhash rejected it. | Report the full error. |
TRANSACTION_NOT_FOUND | Voidhash accepted the transaction, but the SDK no longer held it to finish with StoreKit. | Nothing for access, which Voidhash recorded. Report it if it repeats. |
WINDOW_SCENE_NOT_FOUND | No active window scene to show the App Store sheet from. | Call it while a scene is in the foreground. |
PAYWALL_PRESENTER_NOT_AVAILABLE | A development mode purchase ran on a platform without UIKit, such as macOS, so no confirmation sheet can show. | Test development-mode purchases on iOS. |
FAILED_TO_GET_PRODUCTS | getProducts() couldn't reach the App Store. | Retry, or show the screen without prices. |
FAILED_TO_RESTORE_PURCHASES | restorePurchases() couldn't reach the App Store. | Ask the user to retry. |
RECONCILE_TRANSACTIONS_FAILED | restorePurchases() couldn't deliver a receipt, or Voidhash didn't accept it. | Ask the user to retry later. The receipts stay queued. |
INVALID_TRANSACTION | reportTransaction(transactionId:) got an empty ID. | Pass the App Store transaction ID. |
FAILED_TO_SHOW_MANAGE_SUBSCRIPTIONS | showManageSubscriptions(product:) couldn't open the sheet. | Link to the App Store's subscription settings instead. |
FAILED_TO_PRESENT_CODE_REDEMPTION | presentCodeRedemption() couldn't open the sheet. | Let the user try again. |
REFUND_REQUEST_FAILED | appStore.beginRefundRequest(transactionId:) couldn't open the sheet or send the request. | Link to Apple's refund page instead. |
FAILED_TO_GET_SUBSCRIPTION_STATUS | appStore.subscriptionStatus(groupId:) couldn't read the status. | Retry, or read the person's subscriptions. |
FAILED_TO_GET_APP_TRANSACTION | appStore.appTransaction() couldn't read the app transaction. | Retry later. |
Passing an unverified StoreKit result to reportTransaction(_:) rethrows StoreKit's verification
error.
Paywalls and flows
presentFlow never throws. It returns .failed(FlowError); match on the case.
| 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 | The device never loaded this location and can't reach Voidhash, the download failed, or the paywall wasn't ready to show within 15 seconds. | Show your fallback; retry later. |
FLOW_RENDERER_MISSING | options.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 | There was nothing to present from, the view controller you passed can't present, or the paywall can't be drawn. | Present from a visible screen; report the error. |
FLOW_DISABLED | The client is disabled or shut down. | Nothing. |
A purchase started on a paywall reports its failures through onWarning and shows them on the
paywall. A cancelled purchase isn't reported. See When nothing is shown.
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 an object or array value. Nothing changed. | Fix the call. See Identify users. |
IDENTITY_UNAVAILABLE | A VoidhashApiError from a person or flag refresh on a client that can't use its saved identity: the device hasn't been unlocked since it started, or the client was shut down. Nothing was sent. | Read again after the device is unlocked, from Voidhash.shared. |
identify and setPersonAttributes don't throw for network failures or a rejected key: the SDK
saves the write and sends it later. They throw a VoidhashApiError only when Voidhash refuses the
write for good within the 2 seconds the call waits; identify then restores the previous identity.
Network and API
VoidhashApiError maps the HTTP status to a code. These are rarely thrown, since reads fall back to
the cache and writes are queued; they also appear as the code of transport diagnostics.
| Code | When | What to do |
|---|---|---|
AUTHENTICATION_FAILED | Voidhash itself answered 401 or 403: the publishable key was rejected. getCurrentPerson, getFeatureFlags and getPaywall throw it once, when nothing is cached. | Check the key and its project. See API keys. |
INVALID_REQUEST | Voidhash answered 400, for example to an identify it refuses. | Check the arguments. |
NOT_FOUND | Voidhash answered 404. | Check the ID you passed. |
RATE_LIMIT_EXCEEDED | Voidhash answered 429. | Wait retryAfterMilliseconds; don't retry in a loop. |
API_ERROR | Any other error status. | Report the error with its statusCode and tag. |
NETWORK_ERROR | The request never reached Voidhash. | Retry later. |
INVALID_RESPONSE | Voidhash's answer couldn't be read. | Report the error. |
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
Diagnostics report failures the SDK handled itself, such as a delivery it will retry. They never
throw. Receive them with the onDiagnostic option; onWarning gets the
same situations as text. Diagnostic codes lists every code.
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.