Documentation Index
Fetch the complete documentation index at: https://docs.privacyboost.io/llms.txt
Use this file to discover all available pages before exploring further.
Error Handling Patterns
This page covers advanced error handling — classification, telemetry, structured logging, and circuit breakers. For the basics (try/catch, retry, re-auth, user-friendly messages), see the Error Handling guide.
Error Classification
PrivacyBoostDefaults ships an SdkError.category extension that buckets every variant into one of seven categories. Use it to make UI/operational decisions without exhaustively matching every case.
import PrivacyBoost
import PrivacyBoostDefaults
extension SdkError {
var category: ErrorCategory { /* .auth, .network, .validation, .permission, .rateLimit, .server, .internal */ }
var isRetryable: Bool { /* network, rate-limit, retryable api errors */ }
}
| Category | Meaning | Recommended UI | Retry? |
|---|
.auth | Session/auth flow failed | Re-prompt sign-in | No (but re-auth and try once) |
.network | Connectivity / DNS / timeout | ”Check your connection” | Yes, with backoff |
.validation | Input is malformed | Inline form error | No |
.permission | User cancelled / forbidden | Silent dismiss or contact support | No |
.rateLimit | Too many requests | Wait + auto-retry | Yes, after retryAfterMs |
.server | Backend / proof / merkle failure | Generic error toast | Sometimes (check isRetryable) |
.internal | Bug or invariant violation | ”Something went wrong” + report | No |
Structured Logging
Log SDK errors with stable fields so they’re queryable in Splunk / Datadog / OS Console.
import os.log
enum PBLogger {
private static let log = Logger(subsystem: "com.example.app", category: "privacy-boost")
static func error(_ context: String, _ error: Error) {
guard let sdk = error as? SdkError else {
log.error("[\(context, privacy: .public)] non-sdk error: \(error.localizedDescription, privacy: .public)")
return
}
log.error("""
[\(context, privacy: .public)] \
category=\(String(describing: sdk.category), privacy: .public) \
retryable=\(sdk.isRetryable) \
variant=\(self.variantTag(sdk), privacy: .public) \
message=\(self.userMessage(sdk), privacy: .public)
""")
}
private static func variantTag(_ error: SdkError) -> String {
switch error {
case .NotConnected: return "NotConnected"
case .NotAuthenticated: return "NotAuthenticated"
case .AuthenticationFailed: return "AuthenticationFailed"
case .NetworkError: return "NetworkError"
case .WalletError: return "WalletError"
case .SignatureRejected: return "SignatureRejected"
case .InsufficientBalance: return "InsufficientBalance"
case .InvalidAddress: return "InvalidAddress"
case .InvalidAmount: return "InvalidAmount"
case .RateLimited: return "RateLimited"
case .ApiError(let code, _, _): return "ApiError.\(code)"
case .ShieldError(let code, _): return "ShieldError.\(code)"
case .TransferError(let code, _): return "TransferError.\(code)"
default: return "Other"
}
}
private static func userMessage(_ error: SdkError) -> String {
switch error {
case .NetworkError(let m), .WalletError(let m), .InternalError(let m),
.Forbidden(let m), .ResourceNotFound(let m):
return m
case .ApiError(_, let m, _), .ShieldError(_, let m), .TransferError(_, let m):
return m
default:
return ""
}
}
}
privacy: .public on the variant/category fields keeps them queryable in Console.app; the message field is intentionally not marked public if it could include PII — review your own usage.
Error Reporting Integration
Sentry
import Sentry
func reportSDK(_ error: Error, operation: String) {
let event = Event(level: .error)
event.message = SentryMessage(formatted: "PrivacyBoost.\(operation) failed")
if let sdk = error as? SdkError {
event.tags = [
"pb.category": String(describing: sdk.category),
"pb.retryable": String(sdk.isRetryable),
"pb.variant": variantTag(sdk),
]
if case .ApiError(let code, _, _) = sdk {
event.tags?["pb.api_code"] = code
}
}
SentrySDK.capture(event: event)
}
The pb.category tag lets you build a dashboard that groups errors by class — “auth-class errors are spiking” is more actionable than “twelve different variants are spiking.”
Filter Out Noise
User cancellations (.signatureRejected) and validation errors (.invalidAmount, .invalidAddress) aren’t bugs — don’t ship them to your error tracker. Use a beforeSend-style filter:
func shouldReport(_ error: SdkError) -> Bool {
switch error.category {
case .permission, .validation: return false
default: return true
}
}
Operation-Scoped Wrapping
Wrap each meaningful SDK call in a function that handles logging, reporting, and retry — application code stays clean.
@discardableResult
func runSDK<T>(
_ operation: String,
perform: () async throws -> T
) async throws -> T {
do {
return try await perform()
} catch let error as SdkError {
PBLogger.error(operation, error)
if shouldReport(error) {
reportSDK(error, operation: operation)
}
throw error
} catch {
PBLogger.error(operation, error)
reportSDK(error, operation: operation)
throw error
}
}
// Usage
let result = try await runSDK("shield") {
try await sdk.shield(tokenAddress: token, amount: amount)
}
Circuit Breaker
If the backend is degraded, hammering it with retries makes things worse. Wrap operations in a circuit breaker that trips after consecutive server-class failures.
actor CircuitBreaker {
enum State { case closed, open(until: Date), halfOpen }
private var state: State = .closed
private var consecutiveFailures = 0
private let threshold: Int
private let cooldown: TimeInterval
init(threshold: Int = 5, cooldown: TimeInterval = 60) {
self.threshold = threshold
self.cooldown = cooldown
}
func canPass() -> Bool {
switch state {
case .closed, .halfOpen:
return true
case .open(let until):
if Date() >= until {
state = .halfOpen
return true
}
return false
}
}
func recordSuccess() {
consecutiveFailures = 0
state = .closed
}
func recordFailure(_ error: SdkError) {
switch error.category {
case .server, .network:
consecutiveFailures += 1
if consecutiveFailures >= threshold {
state = .open(until: Date().addingTimeInterval(cooldown))
}
default:
break
}
}
}
let breaker = CircuitBreaker()
func guarded<T>(_ op: () async throws -> T) async throws -> T {
guard await breaker.canPass() else {
throw SdkError.NetworkError(message: "circuit open")
}
do {
let result = try await op()
await breaker.recordSuccess()
return result
} catch let error as SdkError {
await breaker.recordFailure(error)
throw error
}
}
Auth/validation/permission failures should not trip the breaker — they’re user-class problems, not service degradation. The classification by category makes that distinction trivial.
Telemetry: Operation Latency by Outcome
Pair errors with timings — slow successes are interesting too.
struct OpMetric {
let operation: String
let durationMs: Double
let outcome: String // "success" | category name
}
func instrument<T>(
_ operation: String,
perform: () async throws -> T,
sink: (OpMetric) -> Void
) async throws -> T {
let start = ContinuousClock.now
do {
let result = try await perform()
let elapsed = ContinuousClock.now - start
sink(OpMetric(
operation: operation,
durationMs: elapsed.milliseconds,
outcome: "success"
))
return result
} catch let error as SdkError {
let elapsed = ContinuousClock.now - start
sink(OpMetric(
operation: operation,
durationMs: elapsed.milliseconds,
outcome: String(describing: error.category)
))
throw error
}
}
extension Duration {
var milliseconds: Double {
let comps = components
return Double(comps.seconds) * 1_000 + Double(comps.attoseconds) / 1e15
}
}
Forward OpMetric to MetricKit, Firebase Performance, or your own analytics pipeline.
Best Practices
- Branch on
.category, not on individual variants — UI logic stays small and stable across SDK upgrades.
- Filter user-class errors out of error reporters —
.permission and .validation are noise.
- Tag every report with
pb.category and pb.variant — makes dashboards actually useful.
- Trip circuit breakers only on
.server and .network — never on auth or validation.
- Pair errors with timings — a slow success that turns into a timeout is worth catching upstream.
Next Steps