Skip to main content

Error Handling

All Angzarr SDKs provide typed errors with introspection methods for programmatic error handling. This enables retry logic, circuit breakers, and user-friendly error messages.


Error Hierarchy

ErrorDescriptionIntrospection
ClientErrorBase class for all SDK errorsAll methods return false
CommandRejectedErrorBusiness logic rejectionis_precondition_failed()
GrpcErrorgRPC transport failureBased on status code
ConnectionErrorConnection failureis_connection_error()
TransportErrorTransport-level failureis_connection_error()
InvalidArgumentErrorInvalid inputis_invalid_argument()
InvalidTimestampErrorTimestamp parse failure

Introspection Methods

All error types provide these introspection methods:

MethodReturns true when
is_not_found()Aggregate doesn't exist (gRPC NOT_FOUND)
is_precondition_failed()Sequence mismatch or business rejection (gRPC FAILED_PRECONDITION)
is_invalid_argument()Invalid command arguments (gRPC INVALID_ARGUMENT)
is_connection_error()Network/transport failure (gRPC UNAVAILABLE)

Usage Examples

use angzarr_client::{ClientError, DomainClient};

match client.aggregate.handle(command).await {
Ok(response) => {
// Process response
}
Err(ClientError::NotFound(msg)) => {
// Aggregate doesn't exist - maybe create it?
log::warn!("Aggregate not found: {}", msg);
}
Err(ClientError::PreconditionFailed(msg)) => {
// Sequence mismatch - refetch and retry
log::warn!("Optimistic lock failure: {}", msg);
}
Err(ClientError::InvalidArgument(msg)) => {
// Bad input - return validation error to user
log::error!("Invalid argument: {}", msg);
}
Err(ClientError::Connection(msg)) => {
// Network error - retry with backoff
log::error!("Connection error: {}", msg);
}
Err(e) => {
// Other errors
log::error!("Unexpected error: {}", e);
}
}

Command Rejection vs Transport Errors

It's important to distinguish between:

TypeCauseAction
CommandRejectedErrorBusiness rule violationShow user-friendly message, don't retry
PreconditionFailed (sequence)Optimistic locking conflictRefetch state and retry
ConnectionErrorNetwork failureRetry with exponential backoff
InvalidArgumentBad input dataFix input, don't retry

Business Rejection Example

User tries to withdraw $500 from account with $100 balance
→ Aggregate rejects: "Insufficient funds"
→ CommandRejectedError with is_precondition_failed() = true
→ Show user: "Insufficient funds for this withdrawal"

Sequence Mismatch Example

Client A reads aggregate at sequence 5
Client B updates aggregate to sequence 6
Client A sends command with sequence 5
→ Coordinator rejects: "Sequence mismatch"
→ GrpcError with is_precondition_failed() = true
→ Client A refetches events, rebuilds state, retries

Retry Strategies

use std::time::Duration;
use tokio::time::sleep;

async fn execute_with_retry(
client: &AggregateClient,
command: CommandBook,
max_retries: u32,
) -> Result<CommandResponse, ClientError> {
let mut attempts = 0;
loop {
match client.handle(command.clone()).await {
Ok(response) => return Ok(response),
Err(e) if e.is_connection_error() && attempts < max_retries => {
attempts += 1;
let delay = Duration::from_millis(100 * 2u64.pow(attempts));
sleep(delay).await;
}
Err(e) => return Err(e),
}
}
}

Next Steps