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
| Error | Description | Introspection |
|---|---|---|
ClientError | Base class for all SDK errors | All methods return false |
CommandRejectedError | Business logic rejection | is_precondition_failed() |
GrpcError | gRPC transport failure | Based on status code |
ConnectionError | Connection failure | is_connection_error() |
TransportError | Transport-level failure | is_connection_error() |
InvalidArgumentError | Invalid input | is_invalid_argument() |
InvalidTimestampError | Timestamp parse failure | — |
Introspection Methods
All error types provide these introspection methods:
| Method | Returns 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
- Rust
- Go
- Python
- Java
- C#
- C++
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);
}
}
import angzarr "github.com/benjaminabbitt/angzarr/client/go"
response, err := client.Handle(ctx, command)
if err != nil {
if clientErr := angzarr.AsClientError(err); clientErr != nil {
if clientErr.IsNotFound() {
// Aggregate doesn't exist - maybe create it?
log.Printf("Aggregate not found: %v", err)
} else if clientErr.IsPreconditionFailed() {
// Sequence mismatch - refetch and retry
log.Printf("Optimistic lock failure: %v", err)
} else if clientErr.IsInvalidArgument() {
// Bad input - return validation error to user
log.Printf("Invalid argument: %v", err)
} else if clientErr.IsConnectionError() {
// Network error - retry with backoff
log.Printf("Connection error: %v", err)
}
}
return err
}
from angzarr_client.errors import GRPCError, ConnectionError, ClientError
try:
response = client.aggregate.handle(command)
except GRPCError as e:
if e.is_not_found():
# Aggregate doesn't exist - maybe create it?
logger.warning(f"Aggregate not found: {e}")
elif e.is_precondition_failed():
# Sequence mismatch - refetch and retry
logger.warning(f"Optimistic lock failure: {e}")
elif e.is_invalid_argument():
# Bad input - return validation error to user
logger.error(f"Invalid argument: {e}")
else:
raise
except ConnectionError as e:
# Network error - retry with backoff
logger.error(f"Connection error: {e}")
import dev.angzarr.client.Errors.*;
try {
CommandResponse response = client.execute(command);
} catch (ClientError e) {
if (e.isNotFound()) {
// Aggregate doesn't exist - maybe create it?
logger.warn("Aggregate not found: {}", e.getMessage());
} else if (e.isPreconditionFailed()) {
// Sequence mismatch - refetch and retry
logger.warn("Optimistic lock failure: {}", e.getMessage());
} else if (e.isInvalidArgument()) {
// Bad input - return validation error to user
logger.error("Invalid argument: {}", e.getMessage());
} else if (e.isConnectionError()) {
// Network error - retry with backoff
logger.error("Connection error: {}", e.getMessage());
} else {
throw e;
}
}
using Angzarr.Client;
try
{
var response = client.Execute(command);
}
catch (ClientError e)
{
if (e.IsNotFound())
{
// Aggregate doesn't exist - maybe create it?
_logger.LogWarning("Aggregate not found: {Message}", e.Message);
}
else if (e.IsPreconditionFailed())
{
// Sequence mismatch - refetch and retry
_logger.LogWarning("Optimistic lock failure: {Message}", e.Message);
}
else if (e.IsInvalidArgument())
{
// Bad input - return validation error to user
_logger.LogError("Invalid argument: {Message}", e.Message);
}
else if (e.IsConnectionError())
{
// Network error - retry with backoff
_logger.LogError("Connection error: {Message}", e.Message);
}
else
{
throw;
}
}
#include <angzarr/client.hpp>
#include <angzarr/errors.hpp>
try {
auto response = client->aggregate()->handle(command);
} catch (const angzarr::ClientError& e) {
if (e.is_not_found()) {
// Aggregate doesn't exist - maybe create it?
std::cerr << "Aggregate not found: " << e.what() << std::endl;
} else if (e.is_precondition_failed()) {
// Sequence mismatch - refetch and retry
std::cerr << "Optimistic lock failure: " << e.what() << std::endl;
} else if (e.is_invalid_argument()) {
// Bad input - return validation error to user
std::cerr << "Invalid argument: " << e.what() << std::endl;
} else if (e.is_connection_error()) {
// Network error - retry with backoff
std::cerr << "Connection error: " << e.what() << std::endl;
} else {
throw;
}
}
Command Rejection vs Transport Errors
It's important to distinguish between:
| Type | Cause | Action |
|---|---|---|
| CommandRejectedError | Business rule violation | Show user-friendly message, don't retry |
| PreconditionFailed (sequence) | Optimistic locking conflict | Refetch state and retry |
| ConnectionError | Network failure | Retry with exponential backoff |
| InvalidArgument | Bad input data | Fix 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
- Rust
- Go
- Python
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),
}
}
}
func executeWithRetry(
ctx context.Context,
client *angzarr.AggregateClient,
command *pb.CommandBook,
maxRetries int,
) (*pb.CommandResponse, error) {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
response, err := client.Handle(ctx, command)
if err == nil {
return response, nil
}
clientErr := angzarr.AsClientError(err)
if clientErr == nil || !clientErr.IsConnectionError() {
return nil, err
}
lastErr = err
time.Sleep(time.Duration(100*(1<<attempt)) * time.Millisecond)
}
return nil, lastErr
}
import time
from angzarr_client.errors import ClientError
def execute_with_retry(client, command, max_retries=3):
for attempt in range(max_retries + 1):
try:
return client.aggregate.handle(command)
except ClientError as e:
if not hasattr(e, 'is_connection_error') or not e.is_connection_error():
raise
if attempt == max_retries:
raise
time.sleep(0.1 * (2 ** attempt))
Next Steps
- Speculative Execution — What-if scenarios without persistence
- Clients — Client types and connection patterns