Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Handling & Exception Reference

Errors raised by the SDK’s clients are subclasses of the sealed AdukiException.


1. Sealed Exception Class Hierarchy

package pro.aduki.core.errors

sealed class AdukiException(
    message: String,
    cause: Throwable? = null
) : Exception(message, cause) {

    class Network(message: String, cause: Throwable? = null, val code: Int? = null) :
        AdukiException(message, cause)

    /** An auth response that could not be used; [refresh] carries a rotated refresh token it did include. */
    class Auth(message: String, cause: Throwable? = null, val refresh: String = "") :
        AdukiException(message, cause)

    class Unauthorized(message: String, cause: Throwable? = null) :
        AdukiException(message, cause)

    /**
     * A failure envelope from an Aduki service (HTTP status, stable dotted kind, e.g. entity.conflict, rate.limited, sod.refused, raw details JSON, retryAfter seconds).
     */
    class Api(
        val status: Int,
        val kind: String,
        message: String,
        val details: String? = null,
        val retryAfter: Long? = null
    ) : AdukiException("$kind: $message")

    /** The user refused an authorization (device grant access_denied). */
    class Denied(message: String, cause: Throwable? = null) :
        AdukiException(message, cause)

    /** A device code or authorization expired (expired_token or local deadline). */
    class Expired(message: String, cause: Throwable? = null) :
        AdukiException(message, cause)

    class Storage(message: String, cause: Throwable? = null) :
        AdukiException(message, cause)

    class Sync(message: String, cause: Throwable? = null) :
        AdukiException(message, cause)

    class Protocol(message: String, cause: Throwable? = null) :
        AdukiException(message, cause)

    class CircuitOpen(message: String = "Circuit breaker is open") :
        AdukiException(message)
}

2. Exception types

ExceptionRaised whenTypical recovery
UnauthorizedAduki ID refused a sign-in, refresh or unlock (wrong password, missing or wrong second factor, spent, expired or revoked refresh token); a signed-out Id has no refresh token; an OIDC error such as invalid_grantsign in again
Auththe mail or scheduling API answered 401/403; an API key was refused; an Aduki ID 2xx lacked a required field (refresh then holds the rotated token, if any)renew the token (client.refresh()), or fix the account’s rights
ApiAn Aduki service returned a failure envelope with an HTTP status, stable dotted kind (e.g. entity.conflict, rate.limited, sod.refused), raw details JSON, and optional retryAfterbranch on e.kind; wait e.retryAfter seconds if rate limited; refresh on 409 entity.conflict
DeniedThe person refused an authorization (OAuth/device authorization grant access_denied)user opted out; abort flow
ExpiredA device code or authorization expired (expired_token or local deadline)start a new authorization flow
Networktransport failure (timeout, DNS, TLS), or an HTTP error not covered above; code holds the status when there was a response (e.g. 429)retry later; the outbox does this for queued changes
Protocola response that cannot be parsed, a missing mandatory field, or an OIDC check failing (state, issuer, id token)report it; do not retry blindly
CircuitOpena Circuit you use is openwait for the cooldown
Storage, Syncdeclared for local-store and sync failuresthe SDK does not raise them yet

Sign-in messages start with Aduki ID’s error kind where it sent one, for example auth.factor: ....


Error handling in a ViewModel

viewModelScope.launch(Dispatchers.IO) {
    try {
        client.sync.all()
    } catch (e: AdukiException.Auth) {
        // The SDK already renews once on a 401; a second refusal lands here.
        // Step 1: Try an explicit renewal
        val refreshed = client.refresh()
        if (!refreshed) {
            // Step 2: Refresh token is dead — clear session and redirect to login
            client.logout()
            _navEvents.emit(NavDestination.Login)
        }
    } catch (e: AdukiException.CircuitOpen) {
        // Only if you wrap calls in a Circuit: fail fast while the service is down
    } catch (e: AdukiException.Api) {
        // Structured service error: inspect kind and retryAfter
        if (e.kind == "rate.limited" && e.retryAfter != null) {
            Log.w("Aduki", "Rate limited; retry after ${e.retryAfter}s")
        } else {
            Log.e("Aduki", "API failure: ${e.kind} (${e.status}) - ${e.message}")
        }
    } catch (e: AdukiException.Network) {
        // Network blip — Outbox already handles queueing
        Log.w("Aduki", "Network synchronization deferred: ${e.message} (HTTP ${e.code})")
    }
}