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
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.
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.).
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
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
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:
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.
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.
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.
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
- 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