---
title: "Error handling"
description: "Guide to error handling in the Aerospike Rust client, covering Error variants, pattern matching, and stream processing."
---

# Error handling

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/llms.txt)
> 
> All documentation pages available in markdown.

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](https://docs.rs/thiserror) and implements the standard **`std::error::Error`** trait, so you can use `.source()` to walk the cause chain when an error wraps another (such as `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` (such as `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 (such as an 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, such as I/O and parsing errors. |

See the [Error documentation](https://docs.rs/aerospike/latest/aerospike/enum.Error.html) for all variants.

## Pattern matching

Match on the enum and use `..` to ignore extra fields of `ServerError`:

```rust
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`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.new) | [`Client::get`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.get) | [`Record::time_to_live`](https://docs.rs/aerospike/latest/aerospike/struct.Record.html#method.time_to_live) | [`ResultCode`](https://docs.rs/aerospike/latest/aerospike/enum.ResultCode.html)

-   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:

```rust
let record = client.get(&ReadPolicy::default(), &key, Bins::All).await?;
```

> 📖 **API reference**: [`Client::get`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.get)

Callers can still match on the propagated `Error` (such as `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 (for example, if 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`):

```toml
futures = "0.3"
```

```rust
use aerospike::{

    Bins, Error, PartitionFilter,

    QueryPolicy, ResultCode, Statement,

};

use aerospike::query::Filter;

use futures::stream::StreamExt;

let mut stmt = Statement::new("test", "events", Bins::All);

stmt.add_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(Error::ServerError(code, ..)) => {

            eprintln!("server error on record: {code:?}");

        }

        Err(err) => {

            eprintln!("record-level error: {err}");

        }

    }

}
```

> 📖 **API reference**: [`Client::query`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query) | [`Recordset::into_stream`](https://docs.rs/aerospike/latest/aerospike/struct.Recordset.html#method.into_stream)

::: note
In the `aerospike` crate 2.1, prefer `Filter::equal` and `Filter::range` for query filters. The legacy `as_eq!` and `as_range!` macros are deprecated and may fail to compile.
:::

## Using with `anyhow`

`aerospike::Error` converts into [`anyhow::Error`](https://docs.rs/anyhow/latest/anyhow/) automatically, so `?` works:

```rust
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`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.get)

You can attach context with [`anyhow::Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html):

```rust
use anyhow::Context;

let record = client.get(&ReadPolicy::default(), &key, Bins::All).await

    .context("failed to fetch user record")?;
```

> 📖 **API reference**: [`Client::get`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.get)

Format the error with `{:?}` (Debug) rather than `{}` (Display) to see the full cause chain, including nested IO or network errors:

```rust
if let Err(e) = fetch_user(&client, 42).await {

    eprintln!("{:?}", e);

}
```

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()`:

```rust
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`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.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:

```rust
#[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`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.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):

```rust
// 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`](https://docs.rs/aerospike/latest/aerospike/enum.Error.html)

**Read with a default when the record is missing:** Match on `KeyNotFoundError`, then extract typed bin values with `TryFrom`:

```rust
use aerospike::{as_key, Bins, Client, Error, 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(Error::ServerError(ResultCode::KeyNotFoundError, ..)) => "unknown".to_string(),

    Err(err) => return Err(err.into()),

};

println!("name: {name}");
```

> 📖 **API reference**: [`Client::get`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.get) | [Reading bin values](https://aerospike.com/docs/develop/client/rust/2.1.0/data-types#reading-bin-values)

::: note
Do not use `Value::as_string().ok()` to read a string bin. `as_string()` returns a display `String`, not `Option`. Use `String::try_from(v.clone())` instead.
:::

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

```rust
match client.put(&WritePolicy::default(), &key, &bins).await {

    Ok(()) => {}

    Err(Error::ServerError(ResultCode::KeyExistsError, ..)) => {

        // Record already exists (such as under a 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`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.put) | [`ResultCode`](https://docs.rs/aerospike/latest/aerospike/enum.ResultCode.html)

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.

```rust
match client.put(&write_policy, &key, &bins).await {

    Ok(()) => {}

    Err(e @ Error::ServerError(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.

In your `Cargo.toml` file:

```toml
[dependencies]

aerospike = { version = "2.1.0", features = ["rt-tokio"] }

tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

futures = "0.3"
```

In your `src/main.rs` file:

```rust
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(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(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`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.new) | [`Client::put`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.put) | [`Client::query`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query) | [`Statement::new`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html#method.new) | [`Statement::add_filter`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html#method.add_filter) | [`PartitionFilter::all`](https://docs.rs/aerospike/latest/aerospike/struct.PartitionFilter.html#method.all) | [`Client::close`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.close)

### Expected results

The exact text may vary by server version/configuration, but a typical run looks like:

```text
--- 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](https://aerospike.com/docs/develop/client/rust/2.1.0/best-practices): client reuse, batching, policies, and other recommendations.