Error Handling Patterns: Exceptions vs Sealed Results
TrustWeave uses a hybrid error handling approach: exceptions for programming errors and sealed results for expected failures. This guide explains when to use each pattern and why.
Sealed Results → Expected failures that are part of normal operation
Exception-Based Operations
Operations that throw exceptions are those where failure indicates a programming error or invalid configuration:
DID Creation (createDid())
Why Exceptions? Creating a DID should always succeed if configuration is correct. Failure means:
Invalid configuration (programming error)
Missing required components (KMS not configured)
Method not registered (should be registered at startup)
1
2
3
4
5
6
7
8
9
10
11
try{valdid=trustweave.createDid()// Success - DID created}catch(error:DidException.DidMethodNotRegistered){// Programming error: Method should be registeredprintln("Method not registered: ${error.method}")println("Available: ${error.availableMethods}")}catch(error:DidException){// Configuration errorprintln("Failed to create DID: ${error.message}")}
Credential Issuance (trustWeave.issue { })
Why a sealed result? Issuance can fail for resolver, KMS, schema, or adapter reasons—handle them with when (or getOrThrow() only in tests).
importorg.trustweave.credential.results.IssuanceResult// issuerDid: Did, issuerKeyId: String (e.g. from verificationMethod.extractKeyId())when(valissued=trustWeave.issue{credential{type("PersonCredential")issuer(issuerDid)subject{"name"to"Alice"}}signedBy(issuerDid=issuerDid,keyId=issuerKeyId)}){isIssuanceResult.Success->{valcredential=issued.credential// …}isIssuanceResult.Failure.UnsupportedFormat->println(issued.allErrors.joinToString())isIssuanceResult.Failure.AdapterNotReady->println(issued.allErrors.joinToString())isIssuanceResult.Failure.InvalidRequest->println("Invalid field '${issued.field}': ${issued.reason}")isIssuanceResult.Failure.AdapterError->println("Adapter error: ${issued.reason}")isIssuanceResult.Failure.MultipleFailures->println(issued.allErrors.joinToString())}
Wallet Creation (trustWeave.wallet { })
Why a sealed result? Misconfigured factories, invalid holder DIDs, or storage errors surface as WalletCreationResult.Failure—handle with when or getOrThrow() in tests.
when(valresult=trustWeave.resolveDid("did:key:z6Mk...")){isDidResolutionResult.Success->{println("Resolved: ${result.document.id}")}isDidResolutionResult.Deactivated->{// Per DID Resolution 1.0 §4.4, a deactivated DID resolves to no document at all.// This is NOT the same as "not found" — treat it as a rejection, e.g. for// verification/authorization the identity must not be trusted.println("DID is deactivated: ${result.did.value}")}isDidResolutionResult.Failure.NotFound->{println("DID not found - may be created later")}isDidResolutionResult.Failure.InvalidFormat->{println("Invalid DID: ${result.reason}")}isDidResolutionResult.Failure.MethodNotRegistered->{println("Method not available: ${result.method}")}isDidResolutionResult.Failure.ResolutionError->{println("Resolution error - retry later")}isDidResolutionResult.Failure.OptionsError->{println("Unsupported/invalid resolution options: ${result.reason}")}}
Credential Verification (trustWeave.verify)
Why Sealed Result? Verification can fail for expected reasons:
// ❌ Bad: Using sealed result for programming errorvaldidResult=trustweave.createDid()when(didResult){isSuccess->{...}isFailure->{...}// This is a programming error, not expected}// ✅ Good: Exception for programming errortry{valdid=trustweave.createDid()// Should succeed if configured}catch(error:DidException.DidMethodNotRegistered){// Fix configurationthrowIllegalStateException("Missing DID method registration",error)}
Pattern 2: Sealed Result for Expected Failures
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ Bad: Using exception for expected failuretry{valresolution=trustweave.resolveDid("did:unknown:test")}catch(error:DidException.DidNotFound){// This is expected - DID may not exist}// ✅ Good: Sealed result for expected failurewhen(valresolution=trustweave.resolveDid("did:unknown:test")){isDidResolutionResult.Success->{// DID exists}isDidResolutionResult.Failure.NotFound->{// Expected: DID doesn't existprintln("DID not found - this is normal")}}
When to Use Each Pattern
Use Exceptions When:
Programming Error
Method not registered (should be registered at startup)
// ✅ Good: Handle exceptions for configuration errorstry{valdid=trustweave.createDid()// Exception on config error}catch(error:DidException){// Log and fix configurationlogger.error("DID creation failed",error)throwIllegalStateException("Configuration error",error)}// ✅ Good: Handle sealed results for expected failureswhen(valverification=trustWeave.verify(credential)){isVerificationResult.Valid->{// Proceed}isVerificationResult.Invalid->{// Handle expected failure gracefullylogger.warn("Credential invalid: ${verification.allErrors.joinToString()}")// Show user-friendly message}}
2. Don’t Mix Patterns Incorrectly
1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ Bad: Treating expected failure as exceptiontry{valresolution=trustweave.resolveDid("did:unknown:test")// This can fail normally - shouldn't be exception}catch(error:Exception){// Wrong pattern}// ✅ Good: Using sealed result for expected failurewhen(valresolution=trustweave.resolveDid("did:unknown:test")){isDidResolutionResult.Success->{...}isDidResolutionResult.Failure->{...}// Expected}
3. Use Exhaustive Handling for Sealed Results
1
2
3
4
5
6
7
8
9
// ✅ Good: Compiler ensures all cases handledwhen(valresult=trustWeave.resolveDid(did)){isDidResolutionResult.Success->{...}isDidResolutionResult.Failure.NotFound->{...}isDidResolutionResult.Failure.MethodNotRegistered->{...}isDidResolutionResult.Failure.InvalidFormat->{...}isDidResolutionResult.Failure.ResolutionError->{...}// Compiler error if case missing}