> ## 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

> Advanced error handling — classification, telemetry, and operational patterns

# 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](../guides/error-handling).

## Error Classification

`com.privacyboost.defaults` ships `SdkException.category` and `SdkException.isRetryable` extensions that bucket every variant into one of seven categories. Use them to make UI/operational decisions without exhaustively matching every case.

```kotlin theme={null}
import com.privacyboost.defaults.category
import com.privacyboost.defaults.isRetryable
import com.privacyboost.sdk.ErrorCategory
import com.privacyboost.sdk.SdkException

val cat: ErrorCategory = error.category
//   Auth | Network | Validation | Permission | RateLimit | Server | Internal
val retryable: Boolean = error.isRetryable
//   true for 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 your log pipeline (Logcat → Crashlytics, Datadog, etc.).

```kotlin theme={null}
import android.util.Log
import com.privacyboost.defaults.category
import com.privacyboost.defaults.isRetryable
import com.privacyboost.sdk.SdkException

object PBLogger {
    private const val TAG = "PrivacyBoost"

    fun error(context: String, error: Throwable) {
        if (error !is SdkException) {
            Log.e(TAG, "[$context] non-sdk error", error)
            return
        }
        Log.e(TAG, buildString {
            append("[$context] ")
            append("category=${error.category::class.simpleName} ")
            append("retryable=${error.isRetryable} ")
            append("variant=${variantTag(error)} ")
            append("message=${userMessage(error)}")
        }, error)
    }

    private fun variantTag(e: SdkException): String = when (e) {
        is SdkException.NotConnected -> "NotConnected"
        is SdkException.NotAuthenticated -> "NotAuthenticated"
        is SdkException.AuthenticationFailed -> "AuthenticationFailed"
        is SdkException.NetworkException -> "NetworkError"
        is SdkException.WalletException -> "WalletError"
        is SdkException.SignatureRejected -> "SignatureRejected"
        is SdkException.InsufficientBalance -> "InsufficientBalance"
        is SdkException.InvalidAddress -> "InvalidAddress"
        is SdkException.InvalidAmount -> "InvalidAmount"
        is SdkException.RateLimited -> "RateLimited"
        is SdkException.ApiException -> "ApiError.${e.code}"
        is SdkException.ShieldException -> "ShieldError.${e.code}"
        is SdkException.TransferException -> "TransferError.${e.code}"
        else -> e::class.simpleName ?: "Other"
    }

    private fun userMessage(e: SdkException): String = when (e) {
        is SdkException.NetworkException -> e.message ?: ""
        is SdkException.WalletException -> e.message ?: ""
        is SdkException.ApiException -> e.message
        is SdkException.ShieldException -> e.message
        is SdkException.TransferException -> e.message
        else -> ""
    }
}
```

If your message field could include PII, scrub or omit it from logs.

## Error Reporting Integration

### Firebase Crashlytics

```kotlin theme={null}
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.privacyboost.defaults.category
import com.privacyboost.defaults.isRetryable

fun reportSdk(error: Throwable, operation: String) {
    val crash = FirebaseCrashlytics.getInstance()
    crash.setCustomKey("pb.operation", operation)

    if (error is SdkException) {
        crash.setCustomKey("pb.category", error.category::class.simpleName ?: "Unknown")
        crash.setCustomKey("pb.retryable", error.isRetryable)
        if (error is SdkException.ApiException) {
            crash.setCustomKey("pb.api_code", error.code)
        }
    }

    crash.recordException(error)
}
```

The `pb.category` custom key lets you build dashboards that group errors by class — "auth-class errors are spiking" is more actionable than "twelve different variants are spiking."

### Sentry

```kotlin theme={null}
import io.sentry.Sentry
import io.sentry.SentryEvent
import io.sentry.SentryLevel

fun reportSdk(error: Throwable, operation: String) {
    val event = SentryEvent(error).apply {
        level = SentryLevel.ERROR
        setTag("pb.operation", operation)
        if (error is SdkException) {
            setTag("pb.category", error.category::class.simpleName ?: "Unknown")
            setTag("pb.retryable", error.isRetryable.toString())
        }
    }
    Sentry.captureEvent(event)
}
```

### Filter Out Noise

User cancellations (`SignatureRejected`) and validation errors aren't bugs — don't ship them to your error tracker:

```kotlin theme={null}
import com.privacyboost.sdk.ErrorCategory

fun shouldReport(error: SdkException): Boolean = when (error.category) {
    is ErrorCategory.Permission, is ErrorCategory.Validation -> false
    else -> true
}
```

## Operation-Scoped Wrapping

Wrap each meaningful SDK call so logging, reporting, and retry sit in one place.

```kotlin theme={null}
suspend inline fun <T> runSdk(
    operation: String,
    crossinline block: suspend () -> T,
): T {
    return try {
        block()
    } catch (e: SdkException) {
        PBLogger.error(operation, e)
        if (shouldReport(e)) reportSdk(e, operation)
        throw e
    } catch (e: Throwable) {
        PBLogger.error(operation, e)
        reportSdk(e, operation)
        throw e
    }
}

// Usage
val result = runSdk("shield") {
    sdk.shield(token, 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.

```kotlin theme={null}
import com.privacyboost.sdk.ErrorCategory
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

class CircuitBreaker(
    private val threshold: Int = 5,
    private val cooldown: Duration = 1.minutes,
) {
    private sealed class State {
        object Closed : State()
        data class Open(val until: Instant) : State()
        object HalfOpen : State()
    }

    private val mutex = Mutex()
    private var state: State = State.Closed
    private var consecutiveFailures = 0

    suspend fun canPass(): Boolean = mutex.withLock {
        when (val s = state) {
            is State.Closed, State.HalfOpen -> true
            is State.Open -> {
                if (Clock.System.now() >= s.until) {
                    state = State.HalfOpen
                    true
                } else false
            }
        }
    }

    suspend fun recordSuccess() = mutex.withLock {
        consecutiveFailures = 0
        state = State.Closed
    }

    suspend fun recordFailure(error: SdkException) = mutex.withLock {
        when (error.category) {
            is ErrorCategory.Server, is ErrorCategory.Network -> {
                consecutiveFailures++
                if (consecutiveFailures >= threshold) {
                    state = State.Open(until = Clock.System.now() + cooldown)
                }
            }
            else -> { /* user/validation/permission do not trip the breaker */ }
        }
    }
}

private val breaker = CircuitBreaker()

suspend fun <T> guarded(block: suspend () -> T): T {
    if (!breaker.canPass()) {
        throw SdkException.NetworkException("circuit open")
    }
    return try {
        val result = block()
        breaker.recordSuccess()
        result
    } catch (e: SdkException) {
        breaker.recordFailure(e)
        throw e
    }
}
```

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.

```kotlin theme={null}
data class OpMetric(
    val operation: String,
    val durationMs: Long,
    val outcome: String,
)

suspend inline fun <T> instrument(
    operation: String,
    sink: (OpMetric) -> Unit,
    crossinline block: suspend () -> T,
): T {
    val start = System.nanoTime()
    return try {
        block().also {
            sink(OpMetric(
                operation = operation,
                durationMs = (System.nanoTime() - start) / 1_000_000,
                outcome = "success",
            ))
        }
    } catch (e: SdkException) {
        sink(OpMetric(
            operation = operation,
            durationMs = (System.nanoTime() - start) / 1_000_000,
            outcome = e.category::class.simpleName ?: "Internal",
        ))
        throw e
    }
}
```

Forward `OpMetric` to Firebase Performance, Datadog RUM, 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

* [Performance](./performance)
* [Error Handling guide](../guides/error-handling) — basics
* [Error Handling Concepts](/sdk/concepts/error-handling) — cross-platform error model
