Error handling
Most callers don’t need to match on individual errors. LicenseManager collapses transient failures into state transitions (.invalid, .trial, .licensed) and LicensePromptView surfaces user-facing messages.
If you build your own activation UI, read on.
KeylightError cases
Section titled “KeylightError cases”All throwing entrypoints - activateLicense, validateLicense, deactivateLicense - throw KeylightError. It’s Equatable and Sendable, conforms to LocalizedError, and every case carries a localizedDescription ready to show.
public enum KeylightError: Error, Equatable, Sendable { case notImplemented case networkFailure(String) case invalidResponse case signatureInvalid case storageFailure case rateLimited(retryAfter: TimeInterval) case serverError(status: Int) case clientError(status: Int, message: String?) case timeout}| Case | When it’s thrown | What it usually means |
|---|---|---|
.notImplemented | StoreKitProvider can’t perform a KeylightProvider-only operation, or vice versa | Ship a version check in your provider selection code |
.networkFailure(String) | URLError after all retries exhausted | User is offline, VPN misconfigured, DNS problem |
.invalidResponse | Server returned non-JSON or malformed JSON | Keylight bug or middlebox mutating the response |
.signatureInvalid | Lease failed Ed25519 verification | Key rotation mid-flight, or the app’s trusted keyset doesn’t match Keylight |
.storageFailure | Both Keychain and file persistence failed | Simulator without keychain entitlements; user manually broke filesystem perms |
.rateLimited(TimeInterval) | 429 after retries exhausted | Monthly API quota hit or /activate rate-limited - back off for retryAfter seconds |
.serverError(Int) | 5xx after retries exhausted | Keylight outage; existing cached leases keep working |
.clientError(Int, String?) | Non-retryable 4xx | Bad license key, expired, tenant suspended - show message to the user |
.timeout | Request timed out after retries | Very slow network; retry on user action |
Retry behavior
Section titled “Retry behavior”The SDK already retries 408, 429, 5xx, and transient URLError codes up to 4 attempts with exponential backoff + jitter before throwing. By the time your catch block runs, there’s nothing left to retry automatically.
Handling in your own activation UI
Section titled “Handling in your own activation UI”do { let result = try await provider.activateLicense(key: input) if !result.activated { showError(result.error ?? "Activation failed") }} catch KeylightError.clientError(_, let message) { showError(message ?? "Invalid license key")} catch KeylightError.rateLimited(let retryAfter) { showError("Slow down - try again in \(Int(retryAfter))s.")} catch KeylightError.networkFailure, KeylightError.timeout { showError("Couldn't reach Keylight. Check your connection.")} catch { showError(error.localizedDescription)}Logging in Console.app
Section titled “Logging in Console.app”Every request logs under subsystem dev.keylight with a per-call requestId. Stream live:
log stream --predicate 'subsystem == "dev.keylight"' --style compactGrep for the requestId from an error message to see the full request lifecycle. Full debugging flow: Validation & revalidation.
Related
Section titled “Related”- License lifecycle - how errors land in
LicenseState. - HTTP API - the status codes behind
.clientErrorand.serverError.