Error Handling
The SDK separates failures into three tiers. Handling each in the right place keeps your flows predictable:
| Tier | Mechanism | Examples | Your response |
|---|---|---|---|
| Expected outcomes | Sealed result types | User declined, nothing matched, username not registered | switch and render the appropriate state |
| Recoverable flow errors | RecoverableErrorPrompt | Offer resolve failure, submit failure, transient network error | Render retry/exit UI; the SDK retries or unwinds |
| Exceptional conditions | Thrown exceptions | Misconfiguration, transport failure outside a managed flow | Catch, log, decide on retry |
Tier 1 — Sealed result types
Every flow returns a sealed hierarchy, so the compiler enforces that you handle each case:
| Operation | Result type | Cases |
|---|---|---|
auth.authenticate / register / recover | AuthResult | AuthSuccess, UsernameRecovered, AuthFailure |
acceptOffer | AcceptOfferResult | AcceptOfferSuccess, AcceptOfferDeclined, AcceptOfferFailure |
present | PresentResult | PresentSuccess, PresentDeclined, PresentAddCredentialNeeded, PresentFailure |
handleLink | HandleLinkResult | HandleLinkCredentialOffer, HandleLinkPresentation, HandleLinkExpired, HandleLinkUnsupported, HandleLinkFailed |
Failure cases carry a diagnostic message (and for auth, a typed AuthErrorCode). These messages are diagnostics, not user copy — map them to your own localized strings.
Tier 2 — RecoverableErrorPrompt
During issuance and presentation, retryable failures surface as a RecoverableErrorPrompt instead of ending the flow. Respond RetryRequested() to retry the failed step in place, or FlowAborted() to unwind — in which case the flow returns its failure result to the original caller.
Tier 3 — WalletSdkException
Thrown for conditions that should not be handled as flow outcomes:
class WalletSdkException implements Exception {
final String message;
final Object? cause;
}
Common triggers:
WalletSdk.initializewith an emptybaseUrl- Calling
acceptOfferorpresentwithout a configuredpromptHandlerwhen a user decision is required - Wallet-domain orchestration failures with no recoverable path
try {
final result = await sdk.acceptOffer(request);
// switch on result ...
} on WalletSdkException catch (e) {
log.severe('Wallet flow failed', e.message, e.cause);
showError('Something went wrong. Please try again.');
}
Transport errors from your injected HTTP client (e.g. DioException) propagate as-is outside managed flows — handle them alongside WalletSdkException where you call the SDK.
Prompt cancellation
Two exceptions relate to the prompt contract:
PromptCancelledException(promptId, reason)— thrown by your handler when the user dismisses a prompt without deciding. The SDK unwinds the active flow and releases resources; you typically see the flow end with a declined/failure result rather than the exception itself.PromptTimeoutException(promptId, timeout)— the host did not respond within the dispatch window. Treated like cancellation for flow control, distinguishable for diagnostics.
A StateError from WalletPromptRegistry means a flow raised a prompt type you never registered — a programming error to fix, not to catch.
Auth: expected vs exceptional
WalletAuth deliberately models "not registered", "already registered", "verification failed", and user cancellation as AuthFailure values. Only transport-level errors throw:
try {
final result = await sdk.auth.authenticate(username: username);
switch (result) {
case AuthSuccess(:final session):
onSignedIn(session);
case AuthFailure(:final code, :final cancelled):
if (!cancelled) showAuthError(code); // map AuthErrorCode to copy
case UsernameRecovered():
break;
}
} catch (e) {
// Network/transport only — offer retry.
showError('Connection problem. Check your network and try again.');
}
Testing error paths
package:wallet_sdk/testing.dart provides fakes for exercising failure branches without a backend, and WalletSdk.resetForTest() clears the singleton between tests:
import 'package:wallet_sdk/testing.dart';
setUp(() async {
await WalletSdk.resetForTest();
});
Combine with storageForTesting: MemoryWalletStorage() to run storage-dependent flows entirely in memory.