Skip to content

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:

VariantDescription
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 / BatchLastErrorBatch operation failed for a specific key.
UdfBadResponse(msg)UDF execution failed on the server.
Io, Base64, InvalidUtf8, ParseAddr, ParseInt, PwHashWrapped 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 over e.source() to walk the cause chain. There is no .iter() or .backtrace() on this type; use RUST_BACKTRACE=1 for 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:3000
query completed with 0 row(s)
--- query invalid namespace ---
record-level error: Invalid cluster node: Cannot get appropriate node for namespace: does_not_exist
query completed with 0 row(s)

See also

  • Best practices — client reuse, batching, policies, and other recommendations.
Feedback

Was this page helpful?

What type of feedback are you giving?

What would you like us to know?

+Capture screenshot

Can we reach out to you?