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):
| Method | Description |
|---|---|
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:
| Variant | Description |
|---|---|
Server { rc, detail } | The server returned an error. Prefer err.server_result_code() over matching this variant directly. |
Connection | Could not reach the cluster. |
Timeout | Client-side timeout. Prefer err.is_client_timeout(). |
InvalidArgument | Invalid arguments (such as an invalid hosts string). |
BatchRow | Internal to batch response parsing. Inspect each row’s BatchRecord from BatchOperation::batch_record() instead of matching this variant. |
MaxErrorRate | Per-node circuit breaker tripped (ClientPolicy.max_error_rate). |
Commit { error_type, verify_records, roll_records } | Multi-record transaction commit failed. |
StreamTerminated | A query record stream was terminated prematurely; the originating cause, if any, is on .source(). |
UdfBadResponse | UDF execution failed on the server. |
Io, Base64, InvalidUtf8, ParseAddr, ParseInt, PwHash, Async | Wrapped 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 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 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: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.