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:
| Surface | What it means | What 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-After | Retry per the condition (Now or WhenAppForeground) |
iOS .newHaapiFlow / RN recovery.kind === 'newHaapiFlow' | Current flow no longer usable; a fresh one will succeed | Construct a new manager and start over |
iOS .nonRecoverable(action) / Android IdsvrHaapiException.Unrecoverable / RN recovery.kind === 'unrecoverable' | Misconfiguration, unsupported platform, access denied | Surface to user/developer; do not auto-retry |
TokenResponse with ErrorTokenResponse carrying an OAuth error code | Server 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.
SDK Layer#
try {
val response = haapiManager.start(coroutineContext)
handleHaapiResponse(response)
} catch (e: IdsvrHaapiException.Retryable) {
scheduleRetry(e.condition) // condition: Now or WhenAppForeground
} catch (e: IdsvrHaapiException.Unrecoverable) {
handleNonRecoverable(e.action) // ModifyConfiguration / InvalidPlatform / IntrospectCause
}
fun handleTokenResponse(tokenResponse: TokenResponse) {
when (tokenResponse) {
is SuccessfulTokenResponse -> persist(tokenResponse)
is ErrorTokenResponse -> handleOAuthError(tokenResponse.error, tokenResponse.errorDescription)
}
}UI Layer#
At the UI Layer, errors arrive in the OauthModel.Error branch of the HaapiFlowActivity result, or as a Throwable in the RESULT_CANCELED data. Wrap that handling with the same dispatch shape — route Throwables through the SDK-Layer try/catch semantics manually.
React Native has only an SDK Layer surface. SDK failures (network, attestation, DPoP, bridge invariants) reject the Promise with a typed HaapiError; OAuth-protocol errors come back inside the resolved TokenResponse. Catch both surfaces in the same async block:
import { isHaapiError, HaapiErrorCode } from 'identityserver.haapi.reactnative.sdk'
import type { HaapiResponse, TokenResponse } from 'identityserver.haapi.reactnative.sdk'
try {
const response: HaapiResponse = await accessor.haapiManager.start()
handleHaapiResponse(response)
} catch (e) {
if (!isHaapiError(e)) throw e
switch (e.recovery.kind) {
case 'retryable':
scheduleRetry(e.recovery.condition) // 'now' or 'whenAppForeground'
break
case 'newHaapiFlow':
restartFlow()
break
case 'unrecoverable':
handleNonRecoverable(e.recovery.action)
break
}
}
function handleTokenResponse(tokenResponse: TokenResponse) {
if (tokenResponse.responseType === 'success') {
persist(tokenResponse)
} else {
handleOAuthError(tokenResponse.error, tokenResponse.errorDescription)
}
}For finer-grained branching, switch on e.code (the stable HaapiErrorCode enum) — see Error Handling (SDK Layer) .
3. Handle OAuth protocol errors by code#
The common codes and recommended responses:
error code | Typical cause | Response |
|---|---|---|
invalid_grant | Auth code or refresh token expired, revoked, or unbound | Drop cached tokens; prompt re-auth. SDK clears DPoP keys automatically. |
invalid_dpop_proof | Client / server binding settings mismatch, or DPoP proof missing on token exchange | Reconcile via Token Binding ’s server/client matrix |
invalid_request | Malformed token request | Inspect the outgoing parameters; usually a client bug |
invalid_client | Client authentication failed (bad secret, expired cert, JWT signature mismatch) | Verify Client Authentication matches the server’s main HAAPI client |
unauthorized_client | Client authenticated but not permitted to use this grant type | Server-side: check grant types and scopes on CIS |
use_dpop_nonce | Server requires a fresh DPoP nonce | The SDK refreshes and retries automatically; surfaces only if the retry also fails |
Pitfalls#
invalid_grantafter a successful refresh. The SDK clears DPoP keys automatically oninvalid_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
Errorcase (iOS) orIdsvrHaapiException(Android), not asErrorTokenResponse. 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.