Skip to main content

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 */ }
}
CategoryMeaningRecommended UIRetry?
.authSession/auth flow failedRe-prompt sign-inNo (but re-auth and try once)
.networkConnectivity / DNS / timeout”Check your connection”Yes, with backoff
.validationInput is malformedInline form errorNo
.permissionUser cancelled / forbiddenSilent dismiss or contact supportNo
.rateLimitToo many requestsWait + auto-retryYes, after retryAfterMs
.serverBackend / proof / merkle failureGeneric error toastSometimes (check isRetryable)
.internalBug or invariant violation”Something went wrong” + reportNo

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

  1. Branch on .category, not on individual variants — UI logic stays small and stable across SDK upgrades.
  2. Filter user-class errors out of error reporters.permission and .validation are noise.
  3. Tag every report with pb.category and pb.variant — makes dashboards actually useful.
  4. Trip circuit breakers only on .server and .network — never on auth or validation.
  5. Pair errors with timings — a slow success that turns into a timeout is worth catching upstream.

Next Steps