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

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

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