How to Handle Errors#

You want a single error-handling layer that classifies every failure the SDK throws into “retry”, “ask the user to start over”, “show a permanent failure”, or “this is an OAuth protocol error from the server”. This page wires the three error categories from Error Handling into a working dispatch.

For the categorization concept, see Error Handling . For per-layer surface details (decision table, common OAuth codes), see Error Handling (SDK Layer) and Error Handling (Driver Layer) .

Prerequisites#

  • A working HAAPI integration (UI Layer or SDK Layer).
  • An understanding of the three categories: retryable, unrecoverable, OAuth protocol.

Step-by-Step#

1. Triage at the error site#

Every error reaches your code as one of four surfaces. Match the surface first, then dispatch:

SurfaceWhat it meansWhat to do
iOS HaapiError with .retryable(condition) / Android IdsvrHaapiException.Retryable / RN HaapiError with recovery.kind === 'retryable'Transient — network blip, fresh DPoP nonce required, 5xx with Retry-AfterRetry per the condition (Now or WhenAppForeground)
iOS .newHaapiFlow / RN recovery.kind === 'newHaapiFlow'Current flow no longer usable; a fresh one will succeedConstruct a new manager and start over
iOS .nonRecoverable(action) / Android IdsvrHaapiException.Unrecoverable / RN recovery.kind === 'unrecoverable'Misconfiguration, unsupported platform, access deniedSurface to user/developer; do not auto-retry
TokenResponse with ErrorTokenResponse carrying an OAuth error codeServer understood and explicitly rejected (invalid_grant, invalid_dpop_proof, etc.)Inspect the error code; see “Common OAuth Error Codes” below

2. Implement the dispatch#

SDK Layer#

private func handleHaapiError(_ error: Error) {
    guard let haapiError = error as? HaapiError else {
        showFatal("Unexpected error: \(error)")
        return
    }
    switch haapiError.recoverySuggestion {
    case .retryable(let condition):
        scheduleRetry(condition)            // condition: .now or .whenAppForeground
    case .newHaapiFlow:
        restartFlow()
    case .nonRecoverable(let action):
        handleNonRecoverable(action)        // .modifyConfiguration / .invalidPlatform / .introspectCause
    }
}

private func handleTokenResponse(_ tokenResponse: TokenResponse) {
    switch tokenResponse {
    case .successfulToken(let success): persist(success)
    case .errorToken(let err):          handleOAuthError(err.error, err.errorDescription)
    case .error(let err):               handleHaapiError(err)
    }
}

UI Layer#

At the UI Layer, errors arrive through HaapiFlowResult.didReceiveError(_:) — the same dispatch applies. Cast the Error to HaapiError and route through handleHaapiError.

3. Handle OAuth protocol errors by code#

The common codes and recommended responses:

error codeTypical causeResponse
invalid_grantAuth code or refresh token expired, revoked, or unboundDrop cached tokens; prompt re-auth. SDK clears DPoP keys automatically.
invalid_dpop_proofClient / server binding settings mismatch, or DPoP proof missing on token exchangeReconcile via Token Binding ’s server/client matrix
invalid_requestMalformed token requestInspect the outgoing parameters; usually a client bug
invalid_clientClient authentication failed (bad secret, expired cert, JWT signature mismatch)Verify Client Authentication matches the server’s main HAAPI client
unauthorized_clientClient authenticated but not permitted to use this grant typeServer-side: check grant types and scopes on CIS
use_dpop_nonceServer requires a fresh DPoP nonceThe SDK refreshes and retries automatically; surfaces only if the retry also fails

Pitfalls#

  • invalid_grant after a successful refresh. The SDK clears DPoP keys automatically on invalid_grant. Treat all held tokens as invalid — re-issuing the same request will not recover. Prompt re-auth from scratch.
  • Double-handling with the token-endpoint listener. If you’ve registered a TokenEndpointResponseListener (see Token Endpoint Response Listener ), errors arrive in both the listener AND the calling method. Decide on one as the user-facing handler and use the other for raw audit only.
  • Treating transport errors as OAuth errors. Transport-level failures (DNS, TLS, socket interruptions) surface as the Error case (iOS) or IdsvrHaapiException (Android), not as ErrorTokenResponse. Confusing these wastes time chasing the wrong root cause.
  • Auto-retry without backoff. On .retryable(.now) / Retryable(Now), retry with exponential backoff — not in a tight loop. The server’s transient state may need seconds, not milliseconds.

Was this helpful?