Skip to content

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.

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
}
CaseWhen it’s thrownWhat it usually means
.notImplementedStoreKitProvider can’t perform a KeylightProvider-only operation, or vice versaShip a version check in your provider selection code
.networkFailure(String)URLError after all retries exhaustedUser is offline, VPN misconfigured, DNS problem
.invalidResponseServer returned non-JSON or malformed JSONKeylight bug or middlebox mutating the response
.signatureInvalidLease failed Ed25519 verificationKey rotation mid-flight, or the app’s trusted keyset doesn’t match Keylight
.storageFailureBoth Keychain and file persistence failedSimulator without keychain entitlements; user manually broke filesystem perms
.rateLimited(TimeInterval)429 after retries exhaustedMonthly API quota hit or /activate rate-limited - back off for retryAfter seconds
.serverError(Int)5xx after retries exhaustedKeylight outage; existing cached leases keep working
.clientError(Int, String?)Non-retryable 4xxBad license key, expired, tenant suspended - show message to the user
.timeoutRequest timed out after retriesVery slow network; retry on user action

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.

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)
}

Every request logs under subsystem dev.keylight with a per-call requestId. Stream live:

log stream --predicate 'subsystem == "dev.keylight"' --style compact

Grep for the requestId from an error message to see the full request lifecycle. Full debugging flow: Validation & revalidation.