Credential Exchange Protocols - Error Handling
Complete guide to error handling for credential exchange protocols.
Overview
All credential exchange operations throw structured exceptions from the ExchangeException hierarchy. These exceptions extend TrustWeaveException and provide:
- Structured error codes for programmatic handling
- Rich context with relevant information
- Type-safe error handling with sealed classes
- Consistent error format across all TrustWeave modules
Exception Hierarchy
All exchange-related exceptions extend ExchangeException, which extends TrustWeaveException. Plugin-specific exceptions are located in their respective plugin modules:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.trustweave.credential.exchange.*
import org.trustweave.credential.exchange.request.ExchangeRequest
import org.trustweave.credential.exchange.result.ExchangeResult
import org.trustweave.credential.identifiers.*
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
)
when (offerResult) {
is ExchangeResult.Success -> {
val offer = offerResult.value
println("Offer created: ${offer.offerId}")
}
is ExchangeResult.Failure.ProtocolNotSupported -> {
println("Protocol: ${offerResult.protocolName}")
println("Available: ${offerResult.availableProtocols}")
}
is ExchangeResult.Failure.OperationNotSupported -> {
println("Operation: ${offerResult.operation}")
println("Supported: ${offerResult.supportedOperations}")
}
is ExchangeResult.Failure.InvalidRequest -> {
println("Invalid request: ${offerResult.reason}")
println("Field: ${offerResult.field}")
}
is ExchangeResult.Failure.NetworkError -> {
println("Network error: ${offerResult.reason}")
}
else -> {
println("Exchange error: $offerResult")
}
}
Exception Module Structure
- Core exceptions (
ExchangeException): Located incredentials/credential-core- Registry errors
- Request validation errors
- Resource not found errors
- Generic/unknown errors
- Plugin-specific exceptions: Located in their respective plugin modules
DidCommException:credentials/plugins/didcommOidc4VciException:credentials/plugins/oidc4vciChapiException:credentials/plugins/chapi
All plugin exceptions extend ExchangeException, ensuring consistent error handling across all protocols.
Error Types
Registry Errors
ExchangeException.ProtocolNotRegistered
When it occurs:
- Calling any registry method with an unregistered protocol name
Error code: PROTOCOL_NOT_REGISTERED
Properties:
protocolName: String- The requested protocol nameavailableProtocols: List<String>- List of available protocol names
Code example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
)
when (offerResult) {
is ExchangeResult.Failure.ProtocolNotSupported -> {
println("Protocol: ${offerResult.protocolName}")
println("Available: ${offerResult.availableProtocols}")
// Output:
// Protocol: ExchangeProtocolName("didcomm")
// Available: []
}
else -> {
// Success or other error
}
}
Solutions:
- Register the protocol before use:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import org.trustweave.credential.exchange.registry.ExchangeProtocolRegistries import org.trustweave.credential.exchange.ExchangeServices val registry = ExchangeProtocolRegistries.default() val didCommService = DidCommFactory.createInMemoryService(kms) { didStr -> DidDocument(id = Did(didStr), verificationMethod = emptyList()) } registry.register(DidCommExchangeProtocol(didCommService)) val exchangeService = ExchangeServices.createExchangeService( protocolRegistry = registry, credentialService = credentialService, didResolver = didResolver ) // Now safe to use val offerResult = exchangeService.offer( ExchangeRequest.Offer( protocolName = "didcomm".requireExchangeProtocolName(), issuerDid = issuerDid, holderDid = holderDid, credentialPreview = preview, options = ExchangeOptions.builder().build() ) )
- Check available protocols:
1 2 3 4 5 6
val available = registry.getSupportedProtocols() val didcommProtocol = "didcomm".requireExchangeProtocolName() if (!available.contains(didcommProtocol)) { println("Protocol not available. Available: $available") // Register protocol or use different protocol }
- Use isRegistered() to check:
1 2 3 4 5
val protocolName = "didcomm".requireExchangeProtocolName() if (!registry.isRegistered(protocolName)) { // Register protocol registry.register(DidCommExchangeProtocol(didCommService)) }
Prevention:
- Always register protocols before use
- Check
registry.isRegistered()before calling methods - Use
registry.getAllProtocolNames()to see available protocols
Request Validation Errors
ExchangeException.MissingRequiredOption
When it occurs:
- A required option is missing from the request
Error code: MISSING_REQUIRED_OPTION
Properties:
optionName: String- The name of the missing optionprotocolName: String?- The protocol name (if applicable)
Code example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = Did("did:key:issuer"),
holderDid = Did("did:key:holder"),
credentialPreview = preview,
options = ExchangeOptions.Empty // Missing 'fromKeyId' and 'toKeyId'
)
)
when (offerResult) {
is ExchangeResult.Failure.InvalidRequest -> {
println("Invalid request: ${offerResult.reason}")
println("Field: ${offerResult.field}")
// Output:
// Invalid request: Missing required option: fromKeyId
// Field: options
}
else -> {
// Success or other error
}
}
Solutions:
- Add the missing option:
1 2 3 4 5 6 7 8 9 10 11 12
val offerResult = exchangeService.offer( ExchangeRequest.Offer( protocolName = "didcomm".requireExchangeProtocolName(), issuerDid = Did("did:key:issuer"), holderDid = Did("did:key:holder"), credentialPreview = preview, options = ExchangeOptions.builder() .addMetadata("fromKeyId", "did:key:issuer#key-1") .addMetadata("toKeyId", "did:key:holder#key-1") .build() ) )
- Check protocol requirements:
- DIDComm requires:
fromKeyId,toKeyId - OIDC4VCI requires:
credentialIssuer
- DIDComm requires:
ExchangeException.OfferNotFound
When it occurs:
- Requesting a credential using a non-existent offer ID
Error code: OFFER_NOT_FOUND
Properties:
offerId: String- The offer ID that was not found
Code example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val requestResult = exchangeService.request(
ExchangeRequest.Request(
protocolName = "didcomm".requireExchangeProtocolName(),
holderDid = Did("did:key:holder"),
issuerDid = Did("did:key:issuer"),
offerId = OfferId("non-existent-offer-id"),
options = ExchangeOptions.builder().build()
)
)
when (requestResult) {
is ExchangeResult.Failure.MessageNotFound -> {
println("Offer ID: ${requestResult.messageId}")
// Output:
// Offer ID: non-existent-offer-id
}
else -> {
// Success or other error
}
}
Solutions:
- Use a valid offer ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// First create an offer val offerResult = exchangeService.offer( ExchangeRequest.Offer( protocolName = "didcomm".requireExchangeProtocolName(), issuerDid = issuerDid, holderDid = holderDid, credentialPreview = preview, options = ExchangeOptions.builder().build() ) ) val offer = when (offerResult) { is ExchangeResult.Success -> offerResult.value else -> throw IllegalStateException("Offer failed: $offerResult") } // Then use the offer ID val requestResult = exchangeService.request( ExchangeRequest.Request( protocolName = "didcomm".requireExchangeProtocolName(), holderDid = holderDid, issuerDid = issuerDid, offerId = offer.offerId, // Use the actual offer ID options = ExchangeOptions.builder().build() ) )
- Store offer IDs:
- Store offer IDs when creating offers
- Use a database or cache to track offers
ExchangeException.RequestNotFound
When it occurs:
- Issuing a credential using a non-existent request ID
Error code: REQUEST_NOT_FOUND
Properties:
requestId: String- The request ID that was not found
Code example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val issueResult = exchangeService.issue(
ExchangeRequest.Issue(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = Did("did:key:issuer"),
holderDid = Did("did:key:holder"),
credential = credential,
requestId = RequestId("non-existent-request-id"),
options = ExchangeOptions.builder().build()
)
)
when (issueResult) {
is ExchangeResult.Failure.MessageNotFound -> {
println("Request ID: ${issueResult.messageId}")
}
else -> {
// Success or other error
}
}
DIDComm-Specific Errors
DIDComm-specific exceptions are located in the didcomm plugin module and extend ExchangeException:
1
import org.trustweave.credential.didcomm.exception.DidCommException
DidCommException.EncryptionFailed
When it occurs:
- DIDComm message encryption fails
Error code: DIDCOMM_ENCRYPTION_FAILED
Properties:
reason: String- The reason encryption failedfromDid: String?- The sender DID (if available)toDid: String?- The recipient DID (if available)cause: Throwable?- The underlying exception
Code example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
)
when (offerResult) {
is ExchangeResult.Failure.NetworkError -> {
println("Network error: ${offerResult.reason}")
// DIDComm encryption failures are typically network errors
}
is ExchangeResult.Failure.AdapterError -> {
println("Adapter error: ${offerResult.reason}")
// Cryptographic errors are typically adapter errors
}
else -> {
// Success or other error
}
}
Common causes:
- Missing or invalid keys
- Key resolution failure
- Cryptographic operation failure
Solutions:
- Verify keys exist:
1 2
val fromKey = kms.getPublicKey(fromKeyId) val toKey = kms.getPublicKey(toKeyId)
- Check DID resolution:
1 2
val fromDoc = resolveDid(fromDid) val toDoc = resolveDid(toDid)
DidCommException.DecryptionFailed
When it occurs:
- DIDComm message decryption fails
Error code: DIDCOMM_DECRYPTION_FAILED
Properties:
reason: String- The reason decryption failedmessageId: String?- The message ID (if available)cause: Throwable?- The underlying exception
Code example:
1
2
3
4
5
6
7
8
9
import org.trustweave.credential.didcomm.exception.DidCommException
try {
val message = didCommService.unpack(encryptedMessage)
} catch (e: DidCommException.DecryptionFailed) {
println("Error code: ${e.code}")
println("Reason: ${e.reason}")
println("Message ID: ${e.messageId}")
}
DidCommException.PackingFailed
When it occurs:
- DIDComm message packing fails
Error code: DIDCOMM_PACKING_FAILED
Properties:
reason: String- The reason packing failedmessageId: String?- The message ID (if available)cause: Throwable?- The underlying exception
DidCommException.UnpackingFailed
When it occurs:
- DIDComm message unpacking fails
Error code: DIDCOMM_UNPACKING_FAILED
Properties:
reason: String- The reason unpacking failedmessageId: String?- The message ID (if available)cause: Throwable?- The underlying exception
DidCommException.ProtocolError
When it occurs:
- DIDComm protocol error occurs
Error code: DIDCOMM_PROTOCOL_ERROR
Properties:
reason: String- The reason for the errorfield: String?- The field that caused the error (if applicable)cause: Throwable?- The underlying exception
OIDC4VCI-Specific Errors
OIDC4VCI-specific exceptions are located in the oidc4vci plugin module and extend ExchangeException:
1
import org.trustweave.credential.oidc4vci.exception.Oidc4VciException
Oidc4VciException.HttpRequestFailed
When it occurs:
- OIDC4VCI HTTP request fails
Error code: OIDC4VCI_HTTP_REQUEST_FAILED
Properties:
url: String- The URL that was requestedstatusCode: Int?- The HTTP status code (if available)reason: String- The reason the request failedcause: Throwable?- The underlying exception
Code example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "oidc4vci".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder()
.addMetadata("credentialIssuer", "https://issuer.example.com")
.build()
)
)
when (offerResult) {
is ExchangeResult.Failure.NetworkError -> {
println("Network error: ${offerResult.reason}")
// HTTP request failures are network errors
}
else -> {
// Success or other error
}
}
Common causes:
- Network connectivity issues
- Invalid credential issuer URL
- Server errors (5xx)
- Client errors (4xx)
Solutions:
- Check network connectivity:
1 2
// Verify URL is reachable val response = httpClient.newCall(Request.Builder().url(url).build()).execute()
- Verify credential issuer URL:
1
val metadata = oidc4vciService.fetchCredentialIssuerMetadata(credentialIssuerUrl)
Oidc4VciException.TokenExchangeFailed
When it occurs:
- OIDC4VCI token exchange fails
Error code: OIDC4VCI_TOKEN_EXCHANGE_FAILED
Properties:
reason: String- The reason token exchange failedcredentialIssuer: String?- The credential issuer URL (if available)cause: Throwable?- The underlying exception
Code example:
1
2
3
4
5
6
7
8
9
import org.trustweave.credential.oidc4vci.exception.Oidc4VciException
try {
val token = oidc4vciService.exchangeToken(authorizationCode)
} catch (e: Oidc4VciException.TokenExchangeFailed) {
println("Error code: ${e.code}")
println("Reason: ${e.reason}")
println("Issuer: ${e.credentialIssuer}")
}
Oidc4VciException.MetadataFetchFailed
When it occurs:
- OIDC4VCI metadata fetch fails
Error code: OIDC4VCI_METADATA_FETCH_FAILED
Properties:
credentialIssuer: String- The credential issuer URLreason: String- The reason metadata fetch failedcause: Throwable?- The underlying exception
Code example:
1
2
3
4
5
6
7
8
9
import org.trustweave.credential.oidc4vci.exception.Oidc4VciException
try {
val metadata = oidc4vciService.fetchCredentialIssuerMetadata(issuerUrl)
} catch (e: Oidc4VciException.MetadataFetchFailed) {
println("Error code: ${e.code}")
println("Issuer: ${e.credentialIssuer}")
println("Reason: ${e.reason}")
}
Oidc4VciException.CredentialRequestFailed
When it occurs:
- OIDC4VCI credential request fails
Error code: OIDC4VCI_CREDENTIAL_REQUEST_FAILED
Properties:
reason: String- The reason credential request failedcredentialIssuer: String?- The credential issuer URL (if available)cause: Throwable?- The underlying exception
Code example:
1
2
3
4
5
6
7
8
9
import org.trustweave.credential.oidc4vci.exception.Oidc4VciException
try {
val credential = oidc4vciService.requestCredential(accessToken, credentialOffer)
} catch (e: Oidc4VciException.CredentialRequestFailed) {
println("Error code: ${e.code}")
println("Reason: ${e.reason}")
println("Issuer: ${e.credentialIssuer}")
}
ExchangeException.OperationNotSupported
When it occurs:
- Protocol doesn’t support the requested operation
Error code: OPERATION_NOT_SUPPORTED
Properties:
protocolName: String- The protocol nameoperation: String- The requested operationsupportedOperations: List<String>- List of supported operations
Code example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
val proofRequestResult = exchangeService.requestProof(
ProofExchangeRequest.Request(
protocolName = "oidc4vci".requireExchangeProtocolName(),
verifierDid = verifierDid,
proverDid = proverDid,
proofRequest = proofRequest,
options = ExchangeOptions.Empty
)
)
when (proofRequestResult) {
is ExchangeResult.Failure.OperationNotSupported -> {
println("Error code: ${proofRequestResult.code}")
println("Protocol: ${proofRequestResult.protocolName}")
println("Operation: ${proofRequestResult.operation}")
println("Supported: ${proofRequestResult.supportedOperations}")
// Output:
// Error code: OPERATION_NOT_SUPPORTED
// Protocol: ExchangeProtocolName("oidc4vci")
// Operation: REQUEST_PROOF
// Supported: [OFFER_CREDENTIAL, REQUEST_CREDENTIAL, ISSUE_CREDENTIAL]
}
is ExchangeResult.Success -> {
// Handle success
}
else -> {
// Handle other errors
}
}
Solutions:
- Check supported operations:
1 2 3 4 5 6
val protocolName = "oidc4vci".requireExchangeProtocolName() val capabilities = exchangeService.getCapabilities(protocolName) if (capabilities != null) { println("Supported operations: ${capabilities.supportedOperations}") // Output: Supported operations: [OFFER_CREDENTIAL, REQUEST_CREDENTIAL, ISSUE_CREDENTIAL] }
- Use a different protocol:
1 2 3 4 5 6 7 8 9 10
// OIDC4VCI doesn't support proof requests, use DIDComm instead val proofRequestResult = exchangeService.requestProof( ProofExchangeRequest.Request( protocolName = "didcomm".requireExchangeProtocolName(), verifierDid = verifierDid, proverDid = proverDid, proofRequest = proofRequest, options = ExchangeOptions.Empty ) )
- Use a different operation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// If you need proof functionality, use DIDComm or CHAPI val protocolName = "didcomm".requireExchangeProtocolName() if (exchangeService.supports(protocolName)) { val proofRequestResult = exchangeService.requestProof( ProofExchangeRequest.Request( protocolName = protocolName, verifierDid = verifierDid, proverDid = proverDid, proofRequest = proofRequest, options = ExchangeOptions.Empty ) ) } else { println("No protocol available for proof requests") }
Prevention:
- Check
protocol.supportedOperationsbefore calling methods - Use protocol comparison table to choose the right protocol
- Handle
UnsupportedOperationExceptiongracefully
Protocol-Specific Errors
DIDComm Errors
Missing Required Options
When it occurs:
- Missing
fromKeyIdortoKeyIdin options
Error message:
1
Missing required option: fromKeyId
Code example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = Did("did:key:issuer"),
holderDid = Did("did:key:holder"),
credentialPreview = preview,
options = ExchangeOptions.Empty // Missing fromKeyId and toKeyId
)
)
when (offerResult) {
is ExchangeResult.Failure.InvalidRequest -> {
println("Error: ${offerResult.reason}")
}
else -> {
// Success or other error
}
}
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = Did("did:key:issuer"),
holderDid = Did("did:key:holder"),
credentialPreview = preview,
options = ExchangeOptions.builder()
.addMetadata("fromKeyId", "did:key:issuer#key-1") // Required
.addMetadata("toKeyId", "did:key:holder#key-1") // Required
.build()
)
)
val offer = when (offerResult) {
is ExchangeResult.Success -> offerResult.value
else -> throw IllegalStateException("Offer failed: $offerResult")
}
Invalid Key ID Format
When it occurs:
- Key ID doesn’t match expected format
Error message:
1
Invalid key ID format: <key-id>
Solution:
1
2
// Key ID must be in format: did:key:...#key-1
val keyId = "$issuerDid#key-1" // Correct format
DID Resolution Failure
When it occurs:
- Cannot resolve DID to DID document
- DID document doesn’t contain required keys
Error message:
1
Failed to resolve DID: did:key:issuer
Solution:
1
2
3
4
5
6
7
8
9
10
11
// Ensure DID resolver is properly configured
val resolveDid: suspend (String) -> DidDocument? = { did ->
// Implement real DID resolution
yourDidResolver.resolve(did)
}
// Ensure DID document contains required keys
val document = resolveDid("did:key:issuer")
if (document?.verificationMethod?.isEmpty() == true) {
println("DID document has no verification methods")
}
Key Not Found in KMS
When it occurs:
- Key ID references a key that doesn’t exist in KMS
Error message:
1
Key not found: did:key:issuer#key-1
Solution:
1
2
3
4
5
6
// Ensure key exists in KMS before use
val keyId = "did:key:issuer#key-1"
if (!kms.keyExists(keyId)) {
// Create key or use existing key
kms.createKey(keyId, algorithm = "Ed25519")
}
OIDC4VCI Errors
Missing Credential Issuer URL
When it occurs:
- Missing
credentialIssuerin options
Error message:
1
Missing required option: credentialIssuer
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "oidc4vci".requireExchangeProtocolName(),
issuerDid = Did("did:key:issuer"),
holderDid = Did("did:key:holder"),
credentialPreview = preview,
options = ExchangeOptions.builder()
.addMetadata("credentialIssuer", "https://issuer.example.com") // Required
.build()
)
)
val offer = when (offerResult) {
is ExchangeResult.Success -> offerResult.value
else -> throw IllegalStateException("Offer failed: $offerResult")
}
HTTP Request Failure
When it occurs:
- Network error connecting to credential issuer
- Credential issuer returns error response
Error message:
1
HTTP request failed: 404 Not Found
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "oidc4vci".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder()
.addMetadata("credentialIssuer", "https://issuer.example.com")
.build()
)
)
when (offerResult) {
is ExchangeResult.Failure.NetworkError -> {
when {
offerResult.reason?.contains("404") == true -> {
println("Credential issuer not found. Check URL.")
}
offerResult.reason?.contains("401") == true -> {
println("Authentication failed. Check credentials.")
}
offerResult.reason?.contains("timeout") == true -> {
println("Request timed out. Retry later.")
}
else -> {
println("HTTP error: ${offerResult.reason}")
}
}
}
else -> {
// Success or other error
}
}
Token Exchange Failure
When it occurs:
- Authorization code is invalid
- Token exchange endpoint returns error
Error message:
1
Token exchange failed: invalid_grant
Solution:
1
2
3
4
5
// Ensure authorization code is valid and not expired
val options = mapOf(
"credentialIssuer" to "https://issuer.example.com",
"authorizationCode" to validAuthorizationCode // Must be valid and not expired
)
Invalid Credential Issuer Metadata
When it occurs:
- Credential issuer metadata is invalid
- Required endpoints are missing
Error message:
1
Invalid credential issuer metadata: missing token_endpoint
Solution:
1
2
// Verify credential issuer metadata is valid
// Check that all required endpoints are present
CHAPI Errors
Browser Not Available
When it occurs:
- CHAPI requires browser environment
- Running in non-browser context
Error message:
1
CHAPI requires browser environment
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// CHAPI only works in browser
// Use different protocol for server-side operations
if (isBrowserEnvironment()) {
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "chapi".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.Empty
)
)
val offer = when (offerResult) {
is ExchangeResult.Success -> offerResult.value
else -> throw IllegalStateException("Offer failed: $offerResult")
}
} else {
// Use DIDComm or OIDC4VCI for server-side
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
)
val offer = when (offerResult) {
is ExchangeResult.Success -> offerResult.value
else -> throw IllegalStateException("Offer failed: $offerResult")
}
}
Validation Errors
Invalid DID Format
When it occurs:
- DID doesn’t match format:
did:<method>:<identifier>
Error message:
1
Invalid DID format: invalid-did
Solution:
1
2
3
4
5
6
7
8
9
10
// Validate DID format before use
fun isValidDid(did: String): Boolean {
return did.matches(Regex("^did:[a-z0-9]+:.+$"))
}
val issuerDid = "did:key:issuer"
if (!isValidDid(issuerDid)) {
println("Invalid DID format")
return
}
Empty Credential Preview
When it occurs:
credentialPreview.attributesis empty
Error message:
1
Credential preview attributes must not be empty
Solution:
1
2
3
4
5
val preview = CredentialPreview(
attributes = listOf(
CredentialAttribute("name", "Alice") // Must have at least one attribute
)
)
Invalid Credential
When it occurs:
- Credential is missing required fields
- Credential structure is invalid
Error message:
1
Invalid credential: missing issuer
Solution:
1
2
3
4
5
6
7
// Ensure credential has all required fields
val credential = VerifiableCredential(
type = listOf("VerifiableCredential"), // Required
issuer = "did:key:issuer", // Required
credentialSubject = buildJsonObject { }, // Required
issuanceDate = Instant.now().toString() // Required
)
Error Handling Patterns
Pattern 1: Check Before Use
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import org.trustweave.credential.identifiers.*
// Check protocol is registered
val protocolName = "didcomm".requireExchangeProtocolName()
if (!registry.isRegistered(protocolName)) {
println("Protocol not registered")
return
}
// Check operation is supported
val protocol = registry.get(protocolName)
if (protocol?.supportedOperations?.contains(ExchangeOperation.OFFER_CREDENTIAL) != true) {
println("Operation not supported")
return
}
// Now safe to use
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = protocolName,
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
)
val offer = when (offerResult) {
is ExchangeResult.Success -> offerResult.value
else -> throw IllegalStateException("Offer failed: $offerResult")
}
Pattern 2: Result-Based Error Handling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import org.trustweave.credential.exchange.*
import org.trustweave.credential.exchange.request.ExchangeRequest
import org.trustweave.credential.exchange.result.ExchangeResult
import org.trustweave.credential.identifiers.*
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
)
when (offerResult) {
is ExchangeResult.Success -> {
val offer = offerResult.value
println("Offer created: ${offer.offerId}")
}
is ExchangeResult.Failure.ProtocolNotSupported -> {
println("Protocol not registered. Register it first.")
registry.register(DidCommExchangeProtocol(didCommService))
// Retry
val retryResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
)
// Handle retry result
}
is ExchangeResult.Failure.InvalidRequest -> {
println("Invalid request: ${offerResult.reason}")
println("Field: ${offerResult.field}")
}
is ExchangeResult.Failure.OperationNotSupported -> {
println("Operation not supported. Use different protocol or operation.")
println("Supported operations: ${offerResult.supportedOperations}")
}
is ExchangeResult.Failure.NetworkError -> {
println("Network error: ${offerResult.reason}")
// DIDComm encryption/decryption failures are typically network errors
}
is ExchangeResult.Failure.AdapterError -> {
println("Adapter error: ${offerResult.reason}")
// Cryptographic errors are typically adapter errors
}
else -> {
println("Exchange error: $offerResult")
}
}
Pattern 3: Fallback to Alternative Protocol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import org.trustweave.credential.exchange.*
import org.trustweave.credential.exchange.request.ExchangeRequest
import org.trustweave.credential.exchange.result.ExchangeResult
import org.trustweave.credential.identifiers.*
suspend fun offerCredentialWithFallback(
exchangeService: ExchangeService,
registry: ExchangeProtocolRegistry,
preferredProtocol: String,
fallbackProtocols: List<String>,
issuerDid: Did,
holderDid: Did,
credentialPreview: CredentialPreview,
options: ExchangeOptions
): ExchangeResponse.Offer? {
// Try preferred protocol first
val preferredProtocolName = preferredProtocol.requireExchangeProtocolName()
if (registry.isRegistered(preferredProtocolName)) {
val result = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = preferredProtocolName,
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = credentialPreview,
options = options
)
)
when (result) {
is ExchangeResult.Success -> return result.value
else -> println("Preferred protocol failed: $result")
}
}
// Try fallback protocols
for (protocolStr in fallbackProtocols) {
val protocolName = protocolStr.requireExchangeProtocolName()
if (registry.isRegistered(protocolName)) {
val result = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = protocolName,
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = credentialPreview,
options = options
)
)
when (result) {
is ExchangeResult.Success -> return result.value
else -> println("Fallback protocol $protocolStr failed: $result")
}
}
}
return null
}
// Usage
val offer = offerCredentialWithFallback(
exchangeService = exchangeService,
registry = registry,
preferredProtocol = "didcomm",
fallbackProtocols = listOf("oidc4vci", "chapi"),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
) ?: throw IllegalStateException("All protocols failed")
Pattern 4: Validate Before Operation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import org.trustweave.credential.exchange.model.CredentialPreview
import org.trustweave.did.identifiers.Did
fun validateOfferRequest(
issuerDid: Did,
holderDid: Did,
credentialPreview: CredentialPreview
): ValidationResult {
val errors = mutableListOf<String>()
// Validate DIDs (Did type provides basic validation)
if (issuerDid.value.isBlank()) {
errors.add("Invalid issuer DID format")
}
if (holderDid.value.isBlank()) {
errors.add("Invalid holder DID format")
}
// Validate preview
if (credentialPreview.attributes.isEmpty()) {
errors.add("Credential preview attributes must not be empty")
}
// Validate protocol-specific options
// (Implementation depends on protocol)
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
// Use before operation
val validation = validateOfferRequest(issuerDid, holderDid, preview)
if (validation is ValidationResult.Invalid) {
println("Validation failed: ${validation.errors}")
return
}
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
)
val offer = when (offerResult) {
is ExchangeResult.Success -> offerResult.value
else -> throw IllegalStateException("Offer failed: $offerResult")
}
Common Error Scenarios
Scenario 1: Protocol Not Registered
Problem:
1
2
3
4
5
6
7
8
9
10
val registry = ExchangeProtocolRegistries.default()
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
) // Returns ExchangeResult.Failure.ProtocolNotSupported
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
val registry = ExchangeProtocolRegistries.default()
val didCommService = DidCommFactory.createInMemoryService(kms) { didStr ->
DidDocument(id = Did(didStr), verificationMethod = emptyList())
}
registry.register(DidCommExchangeProtocol(didCommService))
val exchangeService = ExchangeServices.createExchangeService(
protocolRegistry = registry,
credentialService = credentialService,
didResolver = didResolver
)
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = ExchangeOptions.builder().build()
)
) // Now works
val offer = when (offerResult) {
is ExchangeResult.Success -> offerResult.value
else -> throw IllegalStateException("Offer failed: $offerResult")
}
Scenario 2: Missing Required Options
Problem:
1
2
3
4
5
6
7
8
9
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = Did("did:key:issuer"),
holderDid = Did("did:key:holder"),
credentialPreview = preview,
options = ExchangeOptions.Empty // Missing fromKeyId and toKeyId
)
) // Returns ExchangeResult.Failure.InvalidRequest
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val offerResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = Did("did:key:issuer"),
holderDid = Did("did:key:holder"),
credentialPreview = preview,
options = ExchangeOptions.builder()
.addMetadata("fromKeyId", "did:key:issuer#key-1") // Added
.addMetadata("toKeyId", "did:key:holder#key-1") // Added
.build()
)
) // Now works
val offer = when (offerResult) {
is ExchangeResult.Success -> offerResult.value
else -> throw IllegalStateException("Offer failed: $offerResult")
}
Scenario 3: Operation Not Supported
Problem:
1
2
3
4
5
6
7
8
9
val proofRequestResult = exchangeService.requestProof(
ExchangeRequest.ProofRequest(
protocolName = "oidc4vci".requireExchangeProtocolName(),
verifierDid = verifierDid,
proverDid = proverDid,
// ... other fields
options = ExchangeOptions.builder().build()
)
) // Returns ExchangeResult.Failure.OperationNotSupported
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Check supported operations first
val protocolName = "oidc4vci".requireExchangeProtocolName()
val protocol = registry.get(protocolName)
if (protocol?.supportedOperations?.contains(ExchangeOperation.REQUEST_PROOF) == true) {
val proofRequestResult = exchangeService.requestProof(
ExchangeRequest.ProofRequest(
protocolName = protocolName,
verifierDid = verifierDid,
proverDid = proverDid,
// ... other fields
options = ExchangeOptions.builder().build()
)
)
} else {
// Use different protocol
val proofRequestResult = exchangeService.requestProof(
ExchangeRequest.ProofRequest(
protocolName = "didcomm".requireExchangeProtocolName(),
verifierDid = verifierDid,
proverDid = proverDid,
// ... other fields
options = ExchangeOptions.builder().build()
)
)
}
Error Recovery Utilities
The ExchangeExceptionRecovery object provides comprehensive error recovery utilities:
Automatic Retry with Exponential Backoff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import org.trustweave.credential.exchange.*
import org.trustweave.credential.exchange.request.ExchangeRequest
import org.trustweave.credential.exchange.result.ExchangeResult
import org.trustweave.credential.identifiers.*
// Automatically retries on transient errors
suspend fun retryOffer(
maxRetries: Int = 3,
exchangeService: ExchangeService,
issuerDid: Did,
holderDid: Did,
preview: CredentialPreview,
options: ExchangeOptions
): ExchangeResponse.Offer {
var lastResult: ExchangeResult<ExchangeResponse.Offer>? = null
repeat(maxRetries) { attempt ->
val result = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = "didcomm".requireExchangeProtocolName(),
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = options
)
)
when (result) {
is ExchangeResult.Success -> return result.value
is ExchangeResult.Failure.NetworkError -> {
lastResult = result
if (attempt < maxRetries - 1) {
kotlinx.coroutines.delay((1 shl attempt) * 1000L) // Exponential backoff
}
}
else -> {
lastResult = result
break
}
}
}
throw IllegalStateException("Offer failed after $maxRetries retries: $lastResult")
}
Error Classification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.trustweave.credential.exchange.result.ExchangeResult
val result: ExchangeResult<*> = // ... get result
// Check if error is retryable
fun isRetryable(result: ExchangeResult<*>): Boolean {
return when (result) {
is ExchangeResult.Failure.NetworkError -> true
is ExchangeResult.Failure.AdapterError -> {
// Check if it's a transient adapter error
result.reason?.contains("timeout") == true ||
result.reason?.contains("temporary") == true
}
else -> false
}
}
if (isRetryable(result)) {
// Retry the operation
}
// Check if error is transient
fun isTransient(result: ExchangeResult<*>): Boolean {
return when (result) {
is ExchangeResult.Failure.NetworkError -> true
else -> false
}
}
if (isTransient(result)) {
// Error might resolve on its own
}
User-Friendly Error Messages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun getUserFriendlyMessage(result: ExchangeResult<*>): String {
return when (result) {
is ExchangeResult.Success -> "Operation succeeded"
is ExchangeResult.Failure.ProtocolNotSupported ->
"Protocol not supported. Available: ${result.availableProtocols}"
is ExchangeResult.Failure.InvalidRequest ->
"Invalid request: ${result.reason}"
is ExchangeResult.Failure.NetworkError ->
"Network error: ${result.reason}. Please check your connection."
else -> "Operation failed: $result"
}
}
val message = getUserFriendlyMessage(result)
println(message) // Displays user-friendly error message
Alternative Protocol Fallback
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
suspend fun tryAlternativeProtocol(
result: ExchangeResult<*>,
availableProtocols: List<String>,
exchangeService: ExchangeService,
issuerDid: Did,
holderDid: Did,
preview: CredentialPreview,
options: ExchangeOptions
): ExchangeResponse.Offer? {
if (result is ExchangeResult.Failure.ProtocolNotSupported) {
for (protocolStr in availableProtocols) {
val protocolName = protocolStr.requireExchangeProtocolName()
val altResult = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = protocolName,
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = options
)
)
when (altResult) {
is ExchangeResult.Success -> return altResult.value
else -> continue
}
}
}
return null
}
Error Recovery Strategies
Strategy 1: Retry with Different Protocol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
suspend fun offerCredentialWithRetry(
exchangeService: ExchangeService,
registry: ExchangeProtocolRegistry,
protocols: List<String>,
issuerDid: Did,
holderDid: Did,
preview: CredentialPreview,
options: ExchangeOptions
): ExchangeResponse.Offer? {
for (protocol in protocols) {
val protocolName = protocol.requireExchangeProtocolName()
if (registry.isRegistered(protocolName)) {
val result = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = protocolName,
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = preview,
options = options
)
)
when (result) {
is ExchangeResult.Success -> return result.value
else -> {
println("Protocol $protocol failed: $result")
continue
}
}
}
}
return null
}
Strategy 2: Register Missing Protocol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import org.trustweave.credential.exchange.*
import org.trustweave.credential.exchange.request.ExchangeRequest
import org.trustweave.credential.exchange.result.ExchangeResult
import org.trustweave.credential.identifiers.*
suspend fun offerCredentialWithAutoRegister(
exchangeService: ExchangeService,
registry: ExchangeProtocolRegistry,
protocolName: String,
issuerDid: Did,
holderDid: Did,
credentialPreview: CredentialPreview,
options: ExchangeOptions
): ExchangeResponse.Offer {
val protocolNameObj = protocolName.requireExchangeProtocolName()
if (!registry.isRegistered(protocolNameObj)) {
// Auto-register protocol
when (protocolName) {
"didcomm" -> {
val service = DidCommFactory.createInMemoryService(kms) { didStr ->
DidDocument(id = Did(didStr), verificationMethod = emptyList())
}
registry.register(DidCommExchangeProtocol(service))
}
"oidc4vci" -> {
val service = Oidc4VciService(credentialIssuerUrl, kms)
registry.register(Oidc4VciExchangeProtocol(service))
}
// ... other protocols
}
}
val result = exchangeService.offer(
ExchangeRequest.Offer(
protocolName = protocolNameObj,
issuerDid = issuerDid,
holderDid = holderDid,
credentialPreview = credentialPreview,
options = options
)
)
return when (result) {
is ExchangeResult.Success -> result.value
else -> throw IllegalStateException("Offer failed: $result")
}
}
Best Practices
- Always check protocol registration:
1 2 3
if (!registry.isRegistered("didcomm")) { // Register or use different protocol }
- Check supported operations:
1 2 3 4
val protocol = registry.get("oidc4vci") if (protocol?.supportedOperations?.contains(ExchangeOperation.OFFER_CREDENTIAL) != true) { // Use different protocol }
- Validate inputs before operations:
1 2 3
if (!isValidDid(issuerDid)) { return // Handle error }
- Use try-catch for all operations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
val offerResult = exchangeService.offer( ExchangeRequest.Offer( protocolName = "didcomm".requireExchangeProtocolName(), issuerDid = issuerDid, holderDid = holderDid, credentialPreview = preview, options = ExchangeOptions.Empty ) ) when (offerResult) { is ExchangeResult.Success -> { // Use offerResult.value } else -> { // Handle error appropriately } }
- Provide helpful error messages:
1 2 3 4
catch (e: ExchangeException) { logger.error("Invalid argument: ${e.message}") // Provide user-friendly message }
Related Documentation
- Quick Start - Get started quickly (5 minutes)
- API Reference - Complete API documentation
- Troubleshooting - Common issues and solutions
- Workflows - Step-by-step workflows
- Examples - Code examples with error handling
- Best Practices - Error handling best practices
- Glossary - Terms and concepts