All errors from measureVitals() are thrown as CircadifyError values. Use Swift’s do/catch with pattern matching to handle specific cases.
public enum CircadifyError: LocalizedError, Equatable {
case cameraPermissionDenied
case captureFailed(String)
case networkError(String)
case uploadFailed(String)
case processingFailed(String)
case rateLimited(retryAfter: Int)
Every case has a human-readable errorDescription and an isRetryable flag.
let result = try await sdk.measureVitals()
} catch let error as CircadifyError {
case .cameraPermissionDenied:
showSettingsPrompt("Camera access is required. Enable it in Settings.")
case .cameraNotAvailable:
showAlert("No camera found on this device.")
showAlert("No face detected. Make sure your face is visible and well-lit.")
showAlert("Scan quality was too low. Try again with better lighting.")
showAlert("Monthly scan limit reached. Upgrade your plan.")
case .rateLimited(let retryAfter):
showAlert("Too many requests. Try again in \(retryAfter) seconds.")
break // User cancelled — no action needed
showRetryPrompt(error.errorDescription ?? "Something went wrong.")
showAlert(error.errorDescription ?? "An error occurred.")
showAlert("Unexpected error: \(error.localizedDescription)")
| Error | Description | Retryable |
|---|
cameraNotAvailable | No camera on this device | No |
cameraPermissionDenied | User denied camera access | No |
cameraInUse | Camera is in use by another app | No |
| Error | Description | Retryable |
|---|
missingApiKey | No API key provided | No |
invalidApiKey | Key is malformed, revoked, or expired | No |
| Error | Description | Retryable |
|---|
captureFailed(String) | Frame capture failed | No |
qualityTooLow | Capture quality too poor for analysis | No |
faceNotDetected | No face found in camera feed | No |
| Error | Description | Retryable |
|---|
networkError(String) | Network request failed | Yes |
uploadFailed(String) | Upload to cloud storage failed | Yes |
apiError(String) | API returned an error | No |
| Error | Description | Retryable |
|---|
sessionExpired | Session timed out | No |
sessionNotFound | Session ID doesn’t exist | No |
| Error | Description | Retryable |
|---|
processingFailed(String) | Server-side inference failed | No |
timeout | Polling for results timed out | Yes |
| Error | Description | Retryable |
|---|
quotaExceeded | Monthly scan limit reached | No |
rateLimited(retryAfter:) | Hourly rate limit exceeded. retryAfter is seconds to wait. | Yes |
| Error | Description | Retryable |
|---|
cancelled | Measurement cancelled via cancel() | No |
unknown(String) | Unexpected error | No |
For retryable errors, wait before retrying:
func measureWithRetry(sdk: CircadifySDK, maxRetries: Int = 3) async throws -> VitalSignsResult {
for attempt in 0..<maxRetries {
return try await sdk.measureVitals()
} catch let error as CircadifyError where error.isRetryable {
if attempt == maxRetries - 1 { throw error }
if case .rateLimited(let retryAfter) = error {
delay = UInt64(retryAfter) * 1_000_000_000
delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000
try await Task.sleep(nanoseconds: delay)
throw CircadifyError.unknown("Max retries exceeded")