Skip to content

Error Handling

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 {
// Camera
case cameraNotAvailable
case cameraPermissionDenied
case cameraInUse
// Configuration
case invalidApiKey
case missingApiKey
// Capture
case captureFailed(String)
case qualityTooLow
case faceNotDetected
// Network
case networkError(String)
case uploadFailed(String)
case apiError(String)
// Session
case sessionExpired
case sessionNotFound
// Processing
case processingFailed(String)
case timeout
// Usage
case quotaExceeded
case rateLimited(retryAfter: Int)
// General
case cancelled
case unknown(String)
}

Every case has a human-readable errorDescription and an isRetryable flag.

do {
let result = try await sdk.measureVitals()
displayResults(result)
} catch let error as CircadifyError {
switch error {
case .cameraPermissionDenied:
showSettingsPrompt("Camera access is required. Enable it in Settings.")
case .cameraNotAvailable:
showAlert("No camera found on this device.")
case .faceNotDetected:
showAlert("No face detected. Make sure your face is visible and well-lit.")
case .qualityTooLow:
showAlert("Scan quality was too low. Try again with better lighting.")
case .quotaExceeded:
showAlert("Monthly scan limit reached. Upgrade your plan.")
case .rateLimited(let retryAfter):
showAlert("Too many requests. Try again in \(retryAfter) seconds.")
case .cancelled:
break // User cancelled — no action needed
default:
if error.isRetryable {
showRetryPrompt(error.errorDescription ?? "Something went wrong.")
} else {
showAlert(error.errorDescription ?? "An error occurred.")
}
}
} catch {
showAlert("Unexpected error: \(error.localizedDescription)")
}
ErrorDescriptionRetryable
cameraNotAvailableNo camera on this deviceNo
cameraPermissionDeniedUser denied camera accessNo
cameraInUseCamera is in use by another appNo
ErrorDescriptionRetryable
missingApiKeyNo API key providedNo
invalidApiKeyKey is malformed, revoked, or expiredNo
ErrorDescriptionRetryable
captureFailed(String)Frame capture failedNo
qualityTooLowCapture quality too poor for analysisNo
faceNotDetectedNo face found in camera feedNo
ErrorDescriptionRetryable
networkError(String)Network request failedYes
uploadFailed(String)Upload to cloud storage failedYes
apiError(String)API returned an errorNo
ErrorDescriptionRetryable
sessionExpiredSession timed outNo
sessionNotFoundSession ID doesn’t existNo
ErrorDescriptionRetryable
processingFailed(String)Server-side inference failedNo
timeoutPolling for results timed outYes
ErrorDescriptionRetryable
quotaExceededMonthly scan limit reachedNo
rateLimited(retryAfter:)Hourly rate limit exceeded. retryAfter is seconds to wait.Yes
ErrorDescriptionRetryable
cancelledMeasurement cancelled via cancel()No
unknown(String)Unexpected errorNo

For retryable errors, wait before retrying:

func measureWithRetry(sdk: CircadifySDK, maxRetries: Int = 3) async throws -> VitalSignsResult {
for attempt in 0..<maxRetries {
do {
return try await sdk.measureVitals()
} catch let error as CircadifyError where error.isRetryable {
if attempt == maxRetries - 1 { throw error }
let delay: UInt64
if case .rateLimited(let retryAfter) = error {
delay = UInt64(retryAfter) * 1_000_000_000
} else {
delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000
}
try await Task.sleep(nanoseconds: delay)
}
}
throw CircadifyError.unknown("Max retries exceeded")
}