Error handling
The Rust client uses the aerospike::Error enum for all errors returned by client methods. Methods return Result<T, aerospike::Error>. The type is defined with thiserror and implements the standard std::error::Error trait, so you can use .source() to walk the cause chain when an error wraps another (e.g. Error::Io from a failed network read).
The Error enum
Important variants:
| Variant | Description |
|---|---|
ServerError(ResultCode, in_doubt, node) | The server returned an error. Use the ResultCode (e.g. KeyNotFoundError, GenerationError, Timeout). Use .. to ignore in_doubt and node if you only care about the code. |
Connection(msg) | Could not reach the cluster. |
Timeout(msg) | Client-side timeout. |
InvalidArgument(msg) | Invalid arguments (e.g. invalid hosts string). |
BatchError / BatchLastError | Batch operation failed for a specific key. |
UdfBadResponse(msg) | UDF execution failed on the server. |
Io, Base64, InvalidUtf8, ParseAddr, ParseInt, PwHash | Wrapped lower-level errors (I/O, parsing, etc.). |
See the Error documentation for all variants.
Pattern matching
Match on the enum and use .. to ignore extra fields of ServerError:
use aerospike::{Client, ClientPolicy, ReadPolicy, Bins, Error, ResultCode, as_key};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let hosts = std::env::var("AEROSPIKE_HOSTS").unwrap_or_else(|_| "127.0.0.1:3000".to_string()); let policy = ClientPolicy::default(); let client = Client::new(&policy, &hosts).await?; let key = as_key!("test", "test", "someKey");
match client.get(&ReadPolicy::default(), &key, Bins::None).await { Ok(record) => { match record.time_to_live() { None => println!("record never expires"), Some(duration) => println!("ttl: {} secs", duration.as_secs()), } } Err(Error::ServerError(ResultCode::KeyNotFoundError, _, _)) => { println!("No such record: {}", key); } Err(err) => { println!("Error fetching record: {}", err); let mut source = std::error::Error::source(&err); while let Some(e) = source { println!("Caused by: {}", e); source = e.source(); } } } Ok(())}📖 API Reference:
Client::new|Client::get|Record::time_to_live|ResultCode
- Use
Error::ServerError(ResultCode::KeyNotFoundError, _, _)(or..) to match server errors. - Use
std::error::Error::source(&err)and a loop overe.source()to walk the cause chain. There is no.iter()or.backtrace()on this type; useRUST_BACKTRACE=1for panic backtraces.
Propagating errors
Propagate with ? and handle at the caller:
let record = client.get(&ReadPolicy::default(), &key, Bins::All).await?;📖 API Reference:
Client::get
Callers can still match on the propagated Error (e.g. KeyNotFoundError, Timeout) as needed.
Handling errors in streams
Query and scan results are consumed as an asynchronous stream. Each item
in the stream is a Result, meaning that individual records can fail independently
(e.g. a single partition times out) without invalidating the rest of the
stream. Match on each item and do not use ? on stream items if you
want to continue processing:
use aerospike::{ as_eq, as_val, Bins, Error, PartitionFilter, QueryPolicy, ResultCode, Statement,};use futures::stream::StreamExt;
let mut stmt = Statement::new("test", "events", Bins::All);stmt.add_filter(as_eq!("type", "click"));
let rs = client .query(&QueryPolicy::default(), PartitionFilter::all(), stmt) .await?;
let mut stream = rs.into_stream();while let Some(item) = stream.next().await { match item { Ok(record) => { println!("{:?}", record.bins); } Err(Error::ServerError(code, ..)) => { eprintln!("server error on record: {code:?}"); } Err(err) => { eprintln!("record-level error: {err}"); } }}📖 API Reference:
Client::query|Recordset::into_stream
Using with anyhow
aerospike::Error converts into anyhow::Error automatically, so ? just works:
async fn fetch_user(client: &Client, id: u64) -> anyhow::Result<Record> { let key = as_key!("test", "users", id); let record = client.get(&ReadPolicy::default(), &key, Bins::All).await?; Ok(record)}📖 API Reference:
Client::get
You can attach context with anyhow::Context:
use anyhow::Context;
let record = client.get(&ReadPolicy::default(), &key, Bins::All).await .context("failed to fetch user record")?;📖 API Reference:
Client::get
anyhow will also automatically include the full error cause chain when displaying errors, so nested IO or network errors are visible without extra work.
Matching on specific errors
Match on aerospike::Error before converting it to anyhow::Error, Box<dyn Error>, or another erased type. Once converted, pattern matching is no longer possible without manual downcasting. Put Ok before Err, and put specific patterns before catch-alls (Rust matches top-to-bottom).
With anyhow: match first, then convert with .into():
async fn get_or_none(client: &Client, key: &Key) -> anyhow::Result<Option<Record>> { match client.get(&ReadPolicy::default(), key, Bins::All).await { Ok(record) => Ok(Some(record)), Err(Error::ServerError(ResultCode::KeyNotFoundError, ..)) => Ok(None), Err(e) => Err(e.into()), }}📖 API Reference:
Client::get
With thiserror: wrap aerospike::Error as a variant of your own error enum. The #[from] attribute lets ? convert automatically, and matching remains fully typed with compiler exhaustiveness checking:
#[derive(Debug, thiserror::Error)]enum AppError { #[error("database error: {0}")] Aerospike(#[from] aerospike::Error), #[error("io error: {0}")] Io(#[from] std::io::Error),}
async fn get_or_none(client: &Client, key: &Key) -> Result<Option<Record>, AppError> { match client.get(&ReadPolicy::default(), key, Bins::All).await { Ok(record) => Ok(Some(record)), Err(Error::ServerError(ResultCode::KeyNotFoundError, ..)) => Ok(None), Err(e) => Err(e.into()), }}📖 API Reference:
Client::get
With Box<dyn Error>: match before erasing. If you need to recover the variant later, use downcast_ref (it returns None if the type doesn’t match):
// Only if you couldn't match earlier:if let Some(ae) = e.downcast_ref::<aerospike::Error>() { match ae { aerospike::Error::ServerError(ResultCode::KeyNotFoundError, ..) => { } _ => {} }}📖 API Reference:
Error
Handling multiple cases (match before converting in all cases):
match client.put(&WritePolicy::default(), &key, &bins).await { Ok(()) => {} Err(Error::ServerError(ResultCode::KeyExistsError, ..)) => { // Record already exists (e.g. create-only policy) } Err(Error::ServerError(ResultCode::GenerationError, ..)) => { // Concurrent modification; retry or conflict resolution } Err(Error::Timeout(..)) => { // Client-side timeout; consider retrying } Err(e) => return Err(e.into()),}📖 API Reference:
Client::put|ResultCode
Complete example
The following program:
- Connects to an Aerospike instance on localhost.
- Writes records into
test/users. - Runs multiple queries, demonstrating pattern matching on
aerospike::Error. - Closes the client connection.
use aerospike::{ as_bin, as_eq, as_key, as_val, Bins, Client, ClientPolicy, Error, PartitionFilter, QueryPolicy, ResultCode, Statement, WritePolicy,};use futures::stream::StreamExt;
async fn seed_data(client: &Client) -> Result<(), Error> { let write_policy = WritePolicy::default();
let users = [ ("user:1", "alice", 34_i64), ("user:2", "bob", 27_i64), ("user:3", "carol", 41_i64), ];
for (id, name, age) in users { let key = as_key!("test", "users", id); let bins = [as_bin!("name", name), as_bin!("age", age)]; client.put(&write_policy, &key, &bins).await?; }
Ok(())}
async fn run_query(label: &str, client: &Client, statement: Statement) { println!("--- {label} ---");
match client .query(&QueryPolicy::default(), PartitionFilter::all(), statement) .await { Ok(recordset) => { let mut rows = 0usize; let mut stream = recordset.into_stream(); while let Some(item) = stream.next().await { match item { Ok(record) => { rows += 1; println!("record #{rows}: {:?}", record.bins); } Err(Error::ServerError(ResultCode::KeyNotFoundError, ..)) => { println!("record-level error: key not found"); } Err(err) => { println!("record-level error: {err}"); } } } println!("query completed with {rows} row(s)"); } Err(Error::ServerError(code, ..)) => { println!("server returned an error code: {code:?}"); } Err(err) => { println!("query failed: {err}"); let mut source = std::error::Error::source(&err); while let Some(cause) = source { println!(" caused by: {cause}"); source = cause.source(); } } }}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let hosts = "127.0.0.1:3000"; let client = Client::new(&ClientPolicy::default(), &hosts).await?;
seed_data(&client).await?;
// Primary-index query: returns records from test/users. let stmt_ok = Statement::new("test", "users", Bins::All); run_query("query all users", &client, stmt_ok).await;
// Secondary-index query on a bin that usually has no index in local dev. // This demonstrates matching ServerError(result_code, ..). let mut stmt_missing_index = Statement::new("test", "users", Bins::All); stmt_missing_index.add_filter(as_eq!("name", "alice")); run_query( "query requiring a secondary index (expected server error without index)", &client, stmt_missing_index, ) .await;
// Query against an invalid namespace to trigger a server-side error. let stmt_bad_namespace = Statement::new("does_not_exist", "users", Bins::All); run_query("query invalid namespace", &client, stmt_bad_namespace).await;
client.close().await?; Ok(())}📖 API Reference:
Client::new|Client::put|Client::query|Statement::new|Statement::add_filter|PartitionFilter::all|Client::close
Expected results
The exact text may vary by server version/configuration, but a typical run looks like:
--- query all users ---record #1: {"name": String("alice"), "age": Int(34)}record #2: {"name": String("bob"), "age": Int(27)}record #3: {"name": String("carol"), "age": Int(41)}query completed with 3 row(s)--- query requiring a secondary index (expected server error without index) ---record-level error: Server error: IndexNotFound, In Doubt: false, Node: 127.0.0.1:3000query completed with 0 row(s)--- query invalid namespace ---record-level error: Invalid cluster node: Cannot get appropriate node for namespace: does_not_existquery completed with 0 row(s)See also
- Best practices — client reuse, batching, policies, and other recommendations.