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 those cached tokens; prompt re-auth. Other sign-ins are unaffected. |
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 |
4. Handle SDK refusals#
One code in the error field does not come from the server at all — the SDK raises it itself:
error code | Typical cause | Response |
|---|---|---|
non_haapi_link | followLink was given a link whose target host or scheme is not the configured identity server. Nothing was transmitted. | Do not retry — the outcome is deterministic. Treat it as a server or reverse-proxy misconfiguration emitting off-host links, and check the configured base URL. |
Classifying links with link.kind before following them (see iOS Representations) avoids most of these, since download, text/html and custom-scheme links are then opened in the browser rather than followed. See Error Handling for the full description.
Pitfalls#
invalid_grantafter a successful refresh. Treat those tokens as invalid — re-issuing the same request will not recover. Prompt re-auth from scratch. The user’s other sign-ins are unaffected.- 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.