Skip to content

Error handling

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

The Rust client uses the aerospike::Error struct for all errors returned by client methods. Methods return Result<T, aerospike::Error>. Error implements the standard std::error::Error trait, so you can use .source() to walk the cause chain when an error wraps another (such as an ErrorKind::Io from a failed network read).

Error is opaque. You cannot match on it directly (there is no Error::Variant(..) syntax). Instead, use its accessor methods, or call Error::kind() to get a &ErrorKind describing the specific failure.

The Error struct and ErrorKind

Important accessors on Error (all drill into the cause chain, so they keep working after an error has been wrapped with retry context):

MethodDescription
kind()Returns the &ErrorKind, the specific failure.
server_result_code()Option<ResultCode> when the server returned a result code (such as KeyNotFoundError, GenerationError).
sub_code() / server_message()Extended server-supplied error detail, when requested with BasePolicy.error_detail_verbosity.
is_client_timeout()true for a client-side timeout.
in_doubt()true when the outcome of a write is uncertain.
node()Last node the command was attempted on, if known.

ErrorKind (returned by kind()) is #[non_exhaustive], so any match on it needs a wildcard _ arm. Important variants:

VariantDescription
Server { rc, detail }The server returned an error. Prefer err.server_result_code() over matching this variant directly.
ConnectionCould not reach the cluster.
TimeoutClient-side timeout. Prefer err.is_client_timeout().
InvalidArgumentInvalid arguments (such as an invalid hosts string).
BatchRowInternal to batch response parsing. Inspect each row’s BatchRecord from BatchOperation::batch_record() instead of matching this variant.
MaxErrorRatePer-node circuit breaker tripped (ClientPolicy.max_error_rate).
Commit { error_type, verify_records, roll_records }Multi-record transaction commit failed.
StreamTerminatedA query record stream was terminated prematurely; the originating cause, if any, is on .source().
UdfBadResponseUDF execution failed on the server.
Io, Base64, InvalidUtf8, ParseAddr, ParseInt, PwHash, AsyncWrapped lower-level errors (I/O, parsing, etc.).

See the Error and ErrorKind documentation for all variants and methods.

Pattern matching

Since Error is a struct and ErrorKind is non-exhaustive, check for a specific server result code with a match guard on server_result_code() instead of matching a variant directly:

#[macro_use]
extern crate aerospike;
use aerospike::{Bins, Client, ClientPolicy, ReadPolicy, ResultCode};
#[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(e) if e.server_result_code() == Some(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 | Error::server_result_code | ResultCode

  • Use a match guard, Err(e) if e.server_result_code() == Some(ResultCode::KeyNotFoundError), to match server errors by result code.
  • 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 check the propagated Error (e.g. with server_result_code() or is_client_timeout()) as needed.

When main returns Result<(), Box<dyn std::error::Error>>, convert aerospike::Error with return Err(e.into()) or ?. Both produce Box<dyn Error>. Do not use Err("message".into()) (a string literal does not convert) or return Err(Box::new(err)) (that boxes the concrete type as Box<aerospike::Error>, which does not satisfy Box<dyn Error>).

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.

Add the futures crate to your Cargo.toml (the Aerospike client uses it internally for streams, but you must declare it to import StreamExt):

futures = "0.3"
use aerospike::{Bins, PartitionFilter, QueryPolicy, Statement};
use aerospike::query::Filter;
use futures::stream::StreamExt;
let mut stmt = Statement::new("test", "events", Bins::All);
stmt.set_filter(Filter::equal("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(err) => match err.server_result_code() {
Some(code) => eprintln!("server error on record: {code:?}"),
None => 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

Format the error with {:#} or {:?} to include the full cause chain, including nested I/O or network errors. The {} (Display) format shows only the outermost message.

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(e) if e.server_result_code() == Some(ResultCode::KeyNotFoundError) => Ok(None),
Err(e) => Err(e.into()),
}
}

📖 API reference: Client::get | Error::server_result_code

With thiserror: wrap aerospike::Error as a variant of your own error enum. The #[from] attribute lets ? convert automatically:

#[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(e) if e.server_result_code() == Some(ResultCode::KeyNotFoundError) => Ok(None),
Err(e) => Err(e.into()),
}
}

📖 API reference: Client::get

With Box<dyn Error>: check before erasing. If you need to recover the concrete type later, use downcast_ref (it returns None if the type doesn’t match):

// Only if you couldn't check earlier:
if let Some(ae) = e.downcast_ref::<aerospike::Error>() {
if ae.server_result_code() == Some(ResultCode::KeyNotFoundError) {
// ...
}
}

📖 API reference: Error

Read with a default when the record is missing: Check for KeyNotFoundError, then extract typed bin values with TryFrom:

use aerospike::{as_key, Bins, Client, ReadPolicy, ResultCode};
let key = as_key!("test", "users", "user:999");
let name = match client.get(&ReadPolicy::default(), &key, Bins::All).await {
Ok(record) => record
.bins
.get("name")
.and_then(|v| String::try_from(v.clone()).ok())
.unwrap_or_else(|| "unknown".to_string()),
Err(e) if e.server_result_code() == Some(ResultCode::KeyNotFoundError) => "unknown".to_string(),
Err(err) => return Err(err.into()),
};
println!("name: {name}");

📖 API reference: Client::get | Reading bin values

Handling multiple cases (match before converting in all cases):

match client.put(&WritePolicy::default(), &key, &bins).await {
Ok(()) => {}
Err(e) if e.server_result_code() == Some(ResultCode::KeyExistsError) => {
// Record already exists (e.g. create-only policy)
}
Err(e) if e.server_result_code() == Some(ResultCode::GenerationError) => {
// Concurrent modification; retry or conflict resolution
}
Err(e) if e.is_client_timeout() => {
// Client-side timeout; consider retrying
}
Err(e) => return Err(e.into()),
}

📖 API reference: Client::put | ResultCode

When main returns Result<(), Box<dyn std::error::Error>>, propagate Aerospike errors with return Err(e.into()) (or ?, which applies the same conversion). That converts aerospike::Error into Box<dyn Error>. Do not use Err("message".into()); a string literal does not convert to Box<dyn Error>, and returning Box<aerospike::Error> directly does not satisfy a Box<dyn Error> return type.

match client.put(&write_policy, &key, &bins).await {
Ok(()) => {}
Err(e) if e.server_result_code() == Some(ResultCode::KeyExistsError) => {
// Create-only policy: record already present; propagate as dyn Error
return Err(e.into());
}
Err(e) => return Err(e.into()),
}

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.
#[macro_use]
extern crate aerospike;
use aerospike::{
as_bin, as_key, Bins, Client,
ClientPolicy, Error, PartitionFilter, QueryPolicy,
ResultCode, Statement, WritePolicy,
};
use aerospike::query::Filter;
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(e) if e.server_result_code() == Some(ResultCode::KeyNotFoundError) => {
println!("record-level error: key not found");
}
Err(err) => {
println!("record-level error: {err}");
}
}
}
println!("query completed with {rows} row(s)");
}
Err(err) => match err.server_result_code() {
Some(code) => {
println!("server returned an error code: {code:?}");
}
None => {
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 checking err.server_result_code().
let mut stmt_missing_index = Statement::new("test", "users", Bins::All);
stmt_missing_index.set_filter(Filter::equal("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::set_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.