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, and similar). |
See the Error and ErrorKind documentation for all variants and methods.
Pattern matching
Because 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 (for example, 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
(for example, when 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 ? works as usual:
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 (for example, 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)Server error details
Aerospike Database 8.2.0 can return a numeric subcode, a message, and, at the highest verbosity, an expression trace on failed operations. Enable this by setting error_detail_verbosity (u8, default 0) on BasePolicy. Use only 0 through 3. The client keeps only the low two bits of the value on the wire, so larger values wrap instead of capping: 4 is sent as 0 (details off), and 5 as 1.
let mut policy = ReadPolicy::default();policy.base_policy.error_detail_verbosity = 2;
match client.get(&policy, &key, Bins::All).await { Ok(record) => println!("{:?}", record.bins), Err(e) => { println!( "status={:?} subcode={} message={:?}", e.server_result_code(), e.sub_code(), e.server_message() ); }}Error::server_result_code()returnsSome(ResultCode)for a server failure.Error::sub_code()returns the numeric subcode (0, orserver_error::sub_code::NONEwhen absent). Dispatch on the(result code, subcode)pair, never the subcode alone. Constants are in theaerospike::server_error::sub_codemodule. See Error subcodes.Error::server_message()returnsSome(&str)with the formatted detail text at verbosity2and up.Error::server_error_detail()returns the fullOption<&ServerErrorDetail>, including the expression trace at verbosity3.BatchRecordcarries the same shape per row:error_detail(),sub_code(), andserver_message().
Expression traces are build-phase only
At verbosity 3, an expression build failure attaches an ExpressionTrace with phase, byte_offset, op, depth, path, and snippet fields, reachable through error.server_error_detail().and_then(|d| d.exp_trace.as_ref()). The Rust client does not decode the eval-phase filter-decision explainer (outcome / operand values) that some other clients expose. phase is always EXP_TRACE_PHASE_BUILD in the traces this client parses. For a client that surfaces the full build-and-eval explainer, see the Go client or Java client.
See Server error details for verbosity levels, coverage, and operator caps.
See also
- Best practices: client reuse, batching, policies, and other recommendations.
- Server error details: structured subcodes, messages, and expression traces (Aerospike Database 8.2.0 and later).