---
title: "Server error details"
description: "Opt in to structured Aerospike Database error subcodes, messages, and expression traces from client libraries."
---

# Server error details

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

This page describes structured server error details and how clients and operators use them.

When Aerospike Database rejects an operation, the client receives a top-level [error code](https://aerospike.com/docs/database/reference/error-codes). Several codes cover more than one failure mode. For example, `AS_ERR_FORBIDDEN` (22) can mean stop-writes, clock skew, truncation in progress, or an XDR write filter block.

## Overview

Starting in Aerospike Database 8.2.0, the server can attach optional structured error details to failure responses: a stable numeric subcode, a short message authored at the failure site, and, for failures that involve an expression, a structured [expression trace](#expression-traces). Clients opt in per request. The default is off, so existing applications keep working unchanged.

### Applies to

-   Database 8.2.0 and later
-   Client libraries that implement error-detail verbosity. See [Client support](#client-support).
-   The operation paths listed in [Coverage](#coverage)

### Audience

-   Developers debugging failed operations or dispatching on `(status, subcode)` pairs
-   Operators who cap error-detail verbosity cluster-wide

### Prerequisites

-   Database 8.2.0 or later on the nodes you query
-   A client library version that exposes error details (see [Client support](#client-support))

### Outcome

After reading this page, you can opt in to structured error details on supported clients, branch on `(status, subcode)` instead of top-level status alone, debug development issues using error messages and expression traces, and use operator settings to limit verbosity when identifiers in error text or resource usage are a concern.

## Client support

Most clients support error details in full. Some do not implement the feature, and some expose only part of it, for example subcodes and messages but not the expression trace. Check the error-handling page for your client for what it supports, and the [client matrix](https://aerospike.com/docs/develop/client-matrix) for the client version that pairs with Database 8.2.0.

The mechanism is the same everywhere: you opt in by setting an error-detail verbosity on the operation policy, and the results come back as fields on the error object or exception your client already raises.

The examples on this page use the [Java client](https://aerospike.com/docs/develop/client/java/error-handling).

The Developer SDKs use typed exceptions and recovery hints appropriate to their language. See [Error handling](https://aerospike.com/docs/develop/client/sdk/concepts/errors).

## How it works

Structured error details use a per-request client opt-in and verbosity level to return optional information about errors encountered while Aerospike processes a client request.

### Client opt-in

Set the error-detail verbosity on the operation policy, for example `errorDetailVerbosity` on `Policy` in the Java client or `error_detail_verbosity` on `as_policy_base` in the C client.

| Level | Server returns |
| --- | --- |
| `0` | Top-level status only (default) |
| `1` | Numeric subcode |
| `2` | Subcode and human-readable message |
| `3` | Subcode, message, and expression trace |

A client might define constants for only some of these levels. Requesting a level your client cannot decode is harmless: the server returns the detail and the client ignores what it does not understand.

### What your client exposes

On a failed operation with error details enabled, the client’s error object carries up to four things:

-   _Status code_: the top-level error code, unchanged from earlier versions.
-   _Subcode_: a stable integer scoped to the status. Absent on the wire when the failure has no dispatch-worthy subcode.
-   _Message_: short text written at the exact failure site in the server, for example `can't create record: namespace or set is truncated`. Returned at verbosity `2` and up.
-   _Expression trace_: a structured description of where an expression failed. Returned at verbosity `3`, only on failures that involve an expression.

In the Java client these are `AerospikeException.getResultCode()`, `getSubCode()`, `getMessage()`, and `getExpressionTrace()`. Other clients name them differently, and some fold the server message into their existing error-message field rather than exposing it separately. See the error-handling page for your client.

On the wire, the details ride in one response field on error responses only. Older clients skip the field so the feature is safe across mixed versions.

### Dispatch on `(status, subcode)`

Subcode integers are scoped to their parent status. The pair `(AS_ERR_PARAMETER, 1)` and `(AS_ERR_FORBIDDEN, 1)` are different conditions. Always branch on both the top-level status and the subcode.

Subcode values are stable: once published, an integer never changes meaning, and retired values are not reused. When the server has no dispatchable subcode for a failure, it omits the subcode entirely. Do not treat `subcode == 0` as a sentinel: subcode numbering starts at `1` for every status, so the server never publishes `0` as a real subcode. The Python client has a constant to represent “no subcode” called `aerospike.SUB_NONE`, while the Go client exposes a named `types.SubCodeNone` constant. The C, Node.js, and Rust clients use the plain integer `0` (Rust names it `sub_code::NONE`). Check for absence using your client’s own idiom rather than hardcoding a comparison against the literal `0`, and confirm a subcode is present before dispatching on its value.

### Operator cap

Use [`error-details-max-verbosity`](https://aerospike.com/docs/database/reference/config#service__error-details-max-verbosity) to limit what clients can receive, regardless of what they request. The server default is `all`. Server values `off`, `codes`, `messages`, and `all` map to client levels `0`, `1`, `2`, and `3`. Effective verbosity is `min(client-requested, server-max)`. The setting is dynamic. Changes apply immediately, with no restart.

## Choosing a verbosity

Error details trade performance for specificity. The happy path is hardly affected: details are built and returned only when a command fails. On failures, though, each verbosity level costs more bandwidth and server CPU than the one below it, up to a cap of about 1 KB of detail per record. When a detail would exceed the cap, the server drops whole parts of the expression trace rather than sending any part of it partially. Long string values in a filter explanation are clipped to 47 bytes with no marker. See [Truncation](#truncation).

Each level serves a different job:

-   _Subcodes (level 1)_ are for your code, and are the only detail meant for regular production use. A subcode adds a few bytes to a failure response. Subcodes are stable integers: dispatch on the `(status, subcode)` pair to route retries, backpressure, or alerts. Do not parse message text in application logic. Wording can change between releases. Subcodes cannot.
-   _Messages (level 2) and expression traces (level 3)_ are for people debugging during development, and interactive tools such as data browsers or LLMs. Use them to see exactly what the server objected to. Because they can contain database content, treat them like record read results: showing them to someone who could read the data directly, such as a developer or operator using a database workbench, is fine and often the point. Think carefully before passing them through to application end users who have no database access of their own.

Leave production applications at level `1` or `0` unless you have a specific reason to run higher, for example, capturing messages temporarily while troubleshooting an incident. An application failing at a high rate pays the detail cost on every error, in response bandwidth and in server-side work to build the details.

::: caution
Error messages can include set names, bin names, and values from your requests. Expression traces can include expression text and, for filter explanations, values read from the record. Error details follow the same permission rules as normal commands: a filter explanation that reveals record contents requires read permission, and data masking still applies, so details never show a client more than it could read directly. To limit exposure further, or to keep a misbehaving application from amplifying its own error traffic, cap details cluster-wide with [`error-details-max-verbosity`](https://aerospike.com/docs/database/reference/config#service__error-details-max-verbosity). A common posture is `all` in development and `codes` or `off` in production.
:::

## Enable error details

Set an error-detail verbosity on the policy you pass to the operation, then read the subcode from the error your client raises. Every client supports all four levels; they differ in whether the levels are named constants and in how the trace is surfaced.

-   [Java SDK](#tab-panel-3757)
-   [Python SDK](#tab-panel-3758)
-   [Rust](#tab-panel-3759)
-   [C#](#tab-panel-3760)
-   [Go](#tab-panel-3761)
-   [Node.js](#tab-panel-3762)
-   [C](#tab-panel-3763)
-   [Java](#tab-panel-3764)
-   [Python](#tab-panel-3765)

```java
Behavior withDetails = Behavior.DEFAULT.deriveWithChanges("withDetails", builder -> builder

    .on(Behavior.Selectors.all(), ops -> ops

        .errorDetailVerbosity(ErrorDetailVerbosity.MESSAGE)

    )

);

try (Cluster cluster = new ClusterDefinition("CLUSTER_HOST", 3000).connect()) {

    Session session = cluster.createSession(withDetails);

    DataSet set = DataSet.of("NAMESPACE_NAME", "SET_NAME");

    try {

        session.upsert(set.id("RECORD_KEY"))

            .bin("BIN_NAME").setTo("BIN_VALUE")

            .execute();

    }

    catch (AerospikeException ae) {

        if (ae.getResultCode() == ResultCode.FAIL_FORBIDDEN &&

            ae.getSubCode() == SubCode.FORBID_TRUNCATED) {

            // The set is mid-truncate, so the record cannot be created.

            // Retry with backoff.

        }

        else {

            throw ae;

        }

    }

}
```

Verbosity levels are named constants on `ErrorDetailVerbosity`. The exception exposes `getResultCode()`, `getSubCode()`, `getMessage()`, and `getExpressionTrace()`.

```python
behavior = Behavior.DEFAULT.derive_with_changes(

    "with_details",

    all=Settings(error_detail_verbosity=ErrorDetailVerbosity.MESSAGE),

)

with ClusterDefinition("CLUSTER_HOST", 3000).connect() as cluster:

    session = cluster.create_session(behavior)

    ds = DataSet.of("NAMESPACE_NAME", "SET_NAME")

    try:

        session.upsert(ds.id("RECORD_KEY")).bin("BIN_NAME").set_to("BIN_VALUE").execute()

    except AerospikeError as e:

        print(e.result_code, e.sub_code, e.server_message)
```

The error exposes `result_code`, `sub_code`, `server_message`, `exp_trace`, and `hint`. `server_message` is `None` at verbosity `0`.

```rust
let mut policy = WritePolicy::default();

policy.base_policy.error_detail_verbosity = 2;

let key = Key::new("NAMESPACE_NAME", "SET_NAME", Value::from("RECORD_KEY"))?;

match client.put(&policy, &key, &[as_bin!("BIN_NAME", "BIN_VALUE")]).await {

    Ok(_) => {}

    Err(e) => println!(

        "{:?} sub_code={} detail={:?}",

        e.server_result_code(),

        e.sub_code(),

        e.server_error_detail().map(|d| d.message.clone())

    ),

}
```

Verbosity is a `u8` on `base_policy`. The error exposes `server_result_code()`, `sub_code()`, and `server_error_detail()`, whose fields are all `Option`.

```csharp
var policy = new WritePolicy();

policy.errorDetailVerbosity = 2;

var key = new Key("NAMESPACE_NAME", "SET_NAME", "RECORD_KEY");

try

{

    client.Put(policy, key, new Bin("BIN_NAME", "BIN_VALUE"));

}

catch (AerospikeException ae)

{

    Console.WriteLine($"{ae.Result} subcode={ae.SubCode} {ae.Message}");

}
```

Verbosity is a plain `int`. The exception exposes `Result`, `SubCode`, `Message`, and `ExpTrace`. Set a cluster-wide default through the client’s public default-policy properties, for example `client.WritePolicyDefault.errorDetailVerbosity`, instead of setting it on every per-call policy.

```go
// Requires: import as "github.com/aerospike/aerospike-client-go/v8"

policy := as.NewWritePolicy(0, 0)

policy.ErrorDetailVerbosity = 2

key, _ := as.NewKey("NAMESPACE_NAME", "SET_NAME", "RECORD_KEY")

if err := client.PutBins(policy, key, as.NewBin("BIN_NAME", "BIN_VALUE")); err != nil {

    ae, _ := err.(*as.AerospikeError)

    fmt.Printf("status=%d subcode=%d %s\n", ae.ResultCode, ae.SubCode, err.Error())

}
```

Verbosity is a plain `int`. `Error` is an interface: assert to `*as.AerospikeError` to reach `ResultCode`, `SubCode`, and `ExpTrace`.

```js
const Aerospike = require('aerospike')

const key = new Aerospike.Key('NAMESPACE_NAME', 'SET_NAME', 'RECORD_KEY')

const policy = { errorDetailVerbosity: Aerospike.errorDetailVerbosity.MESSAGE }

try {

    await client.put(key, { BIN_NAME: 'BIN_VALUE' }, {}, policy)

} catch (error) {

    console.error('status=%d subcode=%d message=%s', error.code, error.subcode, error.message)

}
```

Verbosity levels are on the `Aerospike.errorDetailVerbosity` module. The error object exposes `code`, `subcode`, and `message`.

```c
as_policy_write wp;

as_policy_write_init(&wp);

wp.base.error_detail_verbosity = 2;

as_key key;

as_key_init(&key, "NAMESPACE_NAME", "SET_NAME", "RECORD_KEY");

as_record rec;

as_record_inita(&rec, 1);

as_record_set_str(&rec, "BIN_NAME", "BIN_VALUE");

as_error err;

as_error_init(&err);

if (aerospike_key_put(&as, &err, &wp, &key, &rec) != AEROSPIKE_OK) {

  printf("status=%d subcode=%u %s\n", err.code, err.subcode, err.message);

}

as_record_destroy(&rec);
```

Verbosity is a `uint8_t` on the policy’s `base`. `as_error` carries `code`, `subcode`, and `message`; the expression trace arrives as a suffix on `message`.

```java
AerospikeClient client = new AerospikeClient("CLUSTER_HOST", 3000);

Key key = new Key("NAMESPACE_NAME", "SET_NAME", "RECORD_KEY");

WritePolicy policy = new WritePolicy(client.writePolicyDefault);

policy.errorDetailVerbosity = 2;

try {

    client.put(policy, key, new Bin("BIN_NAME", "BIN_VALUE"));

}

catch (AerospikeException ae) {

    if (ae.getResultCode() == ResultCode.FAIL_FORBIDDEN &&

        ae.getSubCode() == SubCode.FORBID_TRUNCATED) {

        // The set is mid-truncate, so the record cannot be created.

        // Retry with backoff.

    }

    else {

        throw ae;

    }

}

finally {

    client.close();

}
```

Verbosity is a plain `int`. Subcode constants are in the `SubCode` class. When the server returns a message at verbosity `2`, the client surfaces it through `getMessage()`.

```python
policy = {"error_detail_verbosity": aerospike.ERROR_DETAIL_MESSAGE}

key = ("NAMESPACE_NAME", "SET_NAME", "RECORD_KEY")

try:

    client.put(key, {"BIN_NAME": "BIN_VALUE"}, policy=policy)

except ex.AerospikeError as e:

    print(e.code, e.subcode, e.msg)
```

Verbosity levels are named constants: `aerospike.ERROR_DETAIL_NONE`, `_SUBCODE`, `_MESSAGE`, and `_EXP_TRACE`. The exception exposes `code`, `subcode`, and `msg`; the expression trace arrives as a suffix on `msg`.

## Expression traces

Expressions fail in ways a status code cannot explain: a filter built from the wrong types, an operation that hits a type mismatch at evaluation time, or a filter that correctly, but surprisingly, rejects a record. At verbosity `3`, failures that involve an expression include a trace that answers “where, and why”.

The trace tells you:

-   _Phase_: whether the expression failed to build (malformed request) or failed while evaluating against a record.
-   _Location_: the failing operation’s name, its position in the expression, and the chain of operations from the root to the failure.
-   _Snippet_: a rendered view of the failing part of the expression. For expressions written in Aerospike Expression Language (AEL), the snippet points at the offending characters of your source text.
-   _Filter explanations_: for a record rejected by a filter, the trace identifies the comparison that decided the outcome.

Developer SDK authors who write AEL strings can find slot-specific build messages, `aelOffset` / `aelSpan` in source text, and filter explainers on single-key commands in [Debug AEL expressions](https://aerospike.com/docs/develop/client/sdk/concepts/ael/debugging-ael-expressions).

Every trace field is optional. Treat any individual field as best-effort.

### Truncation

An error detail is capped at about 1 KB, and the cap applies per record, so each row of a batch gets its own budget.

When a trace would exceed that budget, the server drops whole parts of it until the rest fits, starting with the operand values of a filter explanation and then the snippet. Parts are dropped whole: the server never sends half a path or half a snippet. If even the core of the trace does not fit, no trace is sent. A very deep path keeps its outer frames and the failing operation, and marks the elided middle with a `...` element.

Because a dropped part is simply an absent field, you cannot tell it from a part that never applied. Operand values are absent whenever something other than a comparison rejected the record, so their absence alone does not mean a trace was truncated.

Separately from that budget, a string value in a filter explanation is clipped to its first 47 bytes, or slightly fewer when 47 bytes would split a multi-byte character. Because the limit is in bytes, a value outside ASCII shows fewer than 47 characters. Numbers, booleans, and `nil` are always shown in full, and collections, blobs, and GeoJSON appear as type placeholders such as `<collection>` rather than as values.

### Reading a trace

At verbosity `3`, the trace comes back in one of two shapes depending on the client. Java, the SDKs, C#, Go, and Rust expose it as a **structured object** with individually addressable fields. C, Node.js, and Python legacy append a **`; exp_trace={...}` suffix** to the error message.

-   [Java SDK](#tab-panel-3766)
-   [Python SDK](#tab-panel-3767)
-   [Rust](#tab-panel-3768)
-   [C#](#tab-panel-3769)
-   [Go](#tab-panel-3770)
-   [Node.js](#tab-panel-3771)
-   [C](#tab-panel-3772)
-   [Java](#tab-panel-3773)
-   [Python](#tab-panel-3774)

```java
Behavior withTrace = Behavior.DEFAULT.deriveWithChanges("withTrace", builder -> builder

    .on(Behavior.Selectors.all(), ops -> ops

        .errorDetailVerbosity(ErrorDetailVerbosity.EXPRESSION_TRACE)

    )

);

Session session = cluster.createSession(withTrace);

try {

    session.query(set.id("RECORD_KEY"))

        .where(Exp.gt(Exp.intBin("age"), Exp.val(21)))

        .failOnFilteredOut()

        .execute();

}

catch (AerospikeException.FilteredException fe) {

    ExpressionTrace trace = fe.getExpressionTrace();

    if (trace != null) {

        System.out.println(trace);

    }

}
```

```python
behavior = Behavior.DEFAULT.derive_with_changes(

    "with_trace",

    all=Settings(error_detail_verbosity=ErrorDetailVerbosity.EXPRESSION_TRACE),

)

session = cluster.create_session(behavior)

try:

    session.query(ds.id("RECORD_KEY")).where("$.age > 21").fail_on_filtered_out().execute()

except FilteredOutError as e:

    print(e.exp_trace)
```

```rust
let mut rp = ReadPolicy::default();

rp.base_policy.error_detail_verbosity = 3;

rp.base_policy.filter_expression = Some(gt(int_bin("age".to_string()), int_val(21)));

if let Err(e) = client.get(&rp, &key, Bins::All).await {

    println!("{:?}", e.server_error_detail().and_then(|d| d.exp_trace.as_ref()));

}
```

```csharp
var policy = new Policy();

policy.errorDetailVerbosity = 3;

policy.filterExp = Exp.Build(Exp.GT(Exp.IntBin("age"), Exp.Val(21)));

policy.failOnFilteredOut = true;

try

{

    client.Get(policy, key);

}

catch (AerospikeException ae)

{

    if (ae.ExpTrace != null)

    {

        Console.WriteLine(ae.ExpTrace);

    }

}
```

```go
// Requires: import as "github.com/aerospike/aerospike-client-go/v8"

rp := as.NewPolicy()

rp.ErrorDetailVerbosity = 3

rp.FilterExpression = as.ExpGreater(as.ExpIntBin("age"), as.ExpIntVal(21))

if _, err := client.Get(rp, key); err != nil {

    ae, _ := err.(*as.AerospikeError)

    if ae.ExpTrace != nil {

        fmt.Printf("%+v\n", *ae.ExpTrace)

    }

}
```

Go returns `FILTERED_OUT` for a non-matching filter without any opt-in.

```js
const Aerospike = require('aerospike')

const policy = new Aerospike.ReadPolicy({

    errorDetailVerbosity: Aerospike.errorDetailVerbosity.EXP_TRACE,

    filterExpression: Aerospike.exp.gt(Aerospike.exp.binInt('age'), Aerospike.exp.int(21))

})

try {

    await client.get(key, policy)

} catch (error) {

    // error.message carries the `; exp_trace={...}` suffix

    console.log(error.message)

}
```

The Node.js client returns `FILTERED_OUT` for a non-matching filter without any opt-in, like Go. It does not decode the trace into a separate field: `error.message` carries the same `; exp_trace={...}` suffix as the C client, since the Node.js binding relays the underlying C client’s message text verbatim. See [Node.js client error handling](https://aerospike.com/docs/develop/client/node/error-handling#server-error-details).

```c
as_exp_build(filter, as_exp_cmp_gt(as_exp_bin_int("age"), as_exp_int(21)));

as_policy_read rp;

as_policy_read_init(&rp);

rp.base.error_detail_verbosity = 3;

rp.base.filter_exp = filter;

as_record* out = NULL;

as_error err;

as_error_init(&err);

if (aerospike_key_get(&as, &err, &rp, &key, &out) != AEROSPIKE_OK) {

  // err.message carries the `; exp_trace={...}` suffix

  printf("%s\n", err.message);

}

as_exp_destroy(filter);
```

```java
Policy policy = new Policy(client.readPolicyDefault);

policy.errorDetailVerbosity = 3;

policy.filterExp = Exp.build(Exp.gt(Exp.intBin("age"), Exp.val(21)));

policy.failOnFilteredOut = true;

try {

    client.get(policy, key);

}

catch (AerospikeException ae) {

    ExpressionTrace trace = ae.getExpressionTrace();

    if (trace != null) {

        System.out.println(trace);

    }

}
```

```python
policy = {

    "error_detail_verbosity": aerospike.ERROR_DETAIL_EXP_TRACE,

    "expressions": GT(IntBin("age"), 21).compile(),

}

try:

    client.get(key, policy=policy)

except ex.AerospikeError as e:

    print(e.msg)   # carries the `; exp_trace={...}` suffix
```

For a record with `age = 15`, the structured clients print:

```text
ExpressionTrace[phase=2, byteOffset=-1, op=gt, depth=1, path=[gt], snippet=gt(bin_int("age"), 21), outcome=2, operands=[15, 21], lang=1]
```

The clients that append to the message print the same trace with **named** values rather than integers:

```text
filtered out - filter expression evaluated to false; exp_trace={phase="eval" op="gt" depth=1 path=["gt"] snippet="gt(bin_int(\"age\"), 21)" outcome="false" operands=["15","21"]}
```

Both describe the same failure. If you are parsing the suffix form, match on `outcome="false"` rather than `outcome=2`.

`phase=2` is the eval phase, `outcome=2` is `OUTCOME_FALSE`, meaning the filter ran and the record genuinely did not match, and `operands` are the two values that decided it: the record’s `age` against the literal `21`. Had the record no `age` bin at all, `outcome` would be `3` (`OUTCOME_ABSENT`) and `operands` would be absent. Individual fields are also available through accessors such as `getOutcome()`, `getOperands()`, `getOp()`, `getPath()`, and `getSnippet()`.

The outcome is worth reading, because a record that did not match, a filter that referenced an absent bin, and a filter that failed to evaluate all arrive as the same `FILTERED_OUT` status.

Reading record values through a trace requires read permission: on a cluster with security enabled, a client authorized only to write does not receive filter explanations at all, not just the operand values, because the outcome and the deciding operation also reveal record contents.

::: caution
A string value in a filter explanation is clipped to 47 bytes, and nothing in the trace marks it as clipped: a 200-character string bin and its first 47 bytes produce identical-looking explanations. Do not treat an operand value as the literal record content, do not compare two operand values for equality, and do not feed one back into a query. Read it as a hint about which comparison decided the outcome, then confirm the value by reading the record.
:::

## Coverage

Database 8.2.0 returns error details for these areas:

| Area | What you get |
| --- | --- |
| Single-record read, write, operate, delete | Subcodes and messages for TTL validation, set limits, truncation, and more. Includes proxied commands: details follow the response back to your client. |
| Batch | Each record in the batch carries its own error detail. A batch where three records fail returns three independent subcode/message pairs. |
| Expressions | Failures in filter expressions and expression operations return messages and, at verbosity `3`, an [expression trace](#expression-traces). |
| Collection data type (CDT) List/Map | Index/rank out of bounds, bounded-list overflow, per-operation messages |
| HLL | Index bits unset, fold/minhash mismatches |
| Bits | Offset/size out of range, resize exceeded |
| String operations | Invalid parameters, regular expressions, UTF-8, and conversion failures |
| Transactions | Record locked, transaction ID mismatch, expiry details |
| User-defined function (UDF), client-invoked | Definition, policy, and execution messages, plus expression traces for a malformed or faulting UDF filter |

Queries return details only when the query fails to start, for example, when the query’s filter expression is malformed. A malformed filter expression on a background UDF or background operate job can also return details before the job starts. Record-level errors and filter outcomes that occur after a query starts do not carry error details.

Not covered: in-job failures for background UDF and background operate jobs, and batch-wide admission errors such as batch queues full.

## Verify

This procedure uses a write with an out-of-range TTL, which the server rejects before it writes anything. The ladder it produces is the same in every client: level `1` adds the subcode, level `2` replaces the client’s generic text with the server’s message.

1.  Confirm the server is not capping verbosity. On any node:
    
    ```text
    asinfo -h CLUSTER_HOST -v "get-config:context=service" | tr ';' '\n' | grep error-details-max-verbosity
    ```
    
    Expect `error-details-max-verbosity=all`, the default. A lower value caps what you see in the steps below.
    
2.  Run the same failing write at each verbosity level:
    
    -   [Java SDK](#tab-panel-3775)
    -   [Python SDK](#tab-panel-3776)
    -   [Rust](#tab-panel-3777)
    -   [C#](#tab-panel-3778)
    -   [Go](#tab-panel-3779)
    -   [Node.js](#tab-panel-3780)
    -   [C](#tab-panel-3781)
    -   [Java](#tab-panel-3782)
    -   [Python](#tab-panel-3783)
    
    ```java
    for (int verbosity = 0; verbosity <= 2; verbosity++) {
    
        final int level = verbosity;
    
        Behavior behavior = Behavior.DEFAULT.deriveWithChanges("verify" + level, builder -> builder
    
            .on(Behavior.Selectors.all(), ops -> ops.errorDetailVerbosity(level)));
    
        Session session = cluster.createSession(behavior);
    
        try {
    
            // One second past the server's 10-year maximum record TTL.
    
            session.upsert(set.id("RECORD_KEY")).bin("v").setTo(1)
    
                .expireRecordAfter(Duration.ofSeconds(315360001)).execute();
    
            System.out.println("verbosity=" + level + " unexpectedly succeeded");
    
        }
    
        catch (AerospikeException ae) {
    
            System.out.println("verbosity=" + level
    
                + " status=" + ae.getResultCode()
    
                + " subcode=" + ae.getSubCode()
    
                + " message=" + ae.getMessage());
    
        }
    
    }
    ```
    
    ```python
    for level in (ErrorDetailVerbosity.NONE,
    
                  ErrorDetailVerbosity.SUBCODE,
    
                  ErrorDetailVerbosity.MESSAGE):
    
        behavior = Behavior.DEFAULT.derive_with_changes(
    
            f"verify{int(level)}", all=Settings(error_detail_verbosity=level))
    
        session = cluster.create_session(behavior)
    
        try:
    
            # One second past the server's 10-year maximum record TTL.
    
            session.upsert(ds.id("RECORD_KEY")).bin("v").set_to(1) \
    
                .expire_record_after(timedelta(seconds=315360001)).execute()
    
            print(f"verbosity={int(level)} unexpectedly succeeded")
    
        except AerospikeError as e:
    
            print(f"verbosity={int(level)} result_code={e.result_code} "
    
                  f"sub_code={e.sub_code} server_message={e.server_message!r}")
    ```
    
    ```rust
    for verbosity in 0u8..=2 {
    
        let mut policy = WritePolicy::default();
    
        policy.base_policy.error_detail_verbosity = verbosity;
    
        // One second past the server's 10-year maximum record TTL.
    
        policy.expiration = aerospike::Expiration::Seconds(315_360_001);
    
        match client.put(&policy, &key, &[as_bin!("v", 1)]).await {
    
            Ok(_) => println!("verbosity={verbosity} unexpectedly succeeded"),
    
            Err(e) => println!(
    
                "verbosity={verbosity} status={:?} sub_code={} detail={:?}",
    
                e.server_result_code(), e.sub_code(),
    
                e.server_error_detail().map(|d| d.message.clone())),
    
        }
    
    }
    ```
    
    ```csharp
    var key = new Key("NAMESPACE_NAME", "SET_NAME", "RECORD_KEY");
    
    for (int verbosity = 0; verbosity <= 2; verbosity++)
    
    {
    
        var policy = new WritePolicy();
    
        policy.errorDetailVerbosity = verbosity;
    
        // One second past the server's 10-year maximum record TTL.
    
        policy.expiration = 315360001;
    
        try
    
        {
    
            client.Put(policy, key, new Bin("v", 1));
    
            Console.WriteLine($"verbosity={verbosity} unexpectedly succeeded");
    
        }
    
        catch (AerospikeException ae)
    
        {
    
            Console.WriteLine($"verbosity={verbosity} status={ae.Result} subcode={ae.SubCode} message={ae.Message}");
    
        }
    
    }
    ```
    
    ```go
    // Requires: import as "github.com/aerospike/aerospike-client-go/v8"
    
    for v := 0; v <= 2; v++ {
    
        policy := as.NewWritePolicy(0, 0)
    
        policy.ErrorDetailVerbosity = v
    
        // One second past the server's 10-year maximum record TTL.
    
        policy.Expiration = 315360001
    
        if err := client.PutBins(policy, key, as.NewBin("v", 1)); err != nil {
    
            ae, _ := err.(*as.AerospikeError)
    
            fmt.Printf("verbosity=%d status=%d subcode=%d message=%s\n",
    
                v, ae.ResultCode, ae.SubCode, err.Error())
    
        } else {
    
            fmt.Printf("verbosity=%d unexpectedly succeeded\n", v)
    
        }
    
    }
    ```
    
    ```js
    const Aerospike = require('aerospike')
    
    for (const v of [
    
        Aerospike.errorDetailVerbosity.NONE,
    
        Aerospike.errorDetailVerbosity.SUBCODE,
    
        Aerospike.errorDetailVerbosity.MESSAGE
    
    ]) {
    
        const policy = { errorDetailVerbosity: v }
    
        try {
    
            // One second past the server's 10-year maximum record TTL.
    
            await client.put(key, { v: 1 }, { ttl: 315360001 }, policy)
    
            console.log(`verbosity=${v} unexpectedly succeeded`)
    
        } catch (error) {
    
            console.log(`verbosity=${v} status=${error.code} subcode=${error.subcode} message=${error.message}`)
    
        }
    
    }
    ```
    
    ```c
    for (uint8_t v = 0; v <= 2; v++) {
    
      as_policy_write wp;
    
      as_policy_write_init(&wp);
    
      wp.base.error_detail_verbosity = v;
    
      as_record rec;
    
      as_record_inita(&rec, 1);
    
      as_record_set_int64(&rec, "v", 1);
    
      // One second past the server's 10-year maximum record TTL.
    
      rec.ttl = 315360001;
    
      as_error e;
    
      as_error_init(&e);
    
      if (aerospike_key_put(&as, &e, &wp, &key, &rec) != AEROSPIKE_OK) {
    
        printf("verbosity=%u status=%d subcode=%u message=%s\n",
    
          v, e.code, e.subcode, e.message);
    
      }
    
      as_record_destroy(&rec);
    
    }
    ```
    
    In the C client the TTL is set on the record, not the write policy.
    
    ```java
    AerospikeClient client = new AerospikeClient("CLUSTER_HOST", 3000);
    
    Key key = new Key("NAMESPACE_NAME", "SET_NAME", "RECORD_KEY");
    
    try {
    
        for (int verbosity = 0; verbosity <= 2; verbosity++) {
    
            WritePolicy policy = new WritePolicy(client.writePolicyDefault);
    
            policy.errorDetailVerbosity = verbosity;
    
            // One second past the server's 10-year maximum record TTL.
    
            policy.expiration = 315360001;
    
            try {
    
                client.put(policy, key, new Bin("v", 1));
    
                System.out.println("verbosity=" + verbosity + " unexpectedly succeeded");
    
            }
    
            catch (AerospikeException ae) {
    
                System.out.println("verbosity=" + verbosity
    
                    + " status=" + ae.getResultCode()
    
                    + " subcode=" + ae.getSubCode()
    
                    + " message=" + ae.getMessage());
    
            }
    
        }
    
    }
    
    finally {
    
        client.close();
    
    }
    ```
    
    ```python
    for level in (aerospike.ERROR_DETAIL_NONE,
    
                  aerospike.ERROR_DETAIL_SUBCODE,
    
                  aerospike.ERROR_DETAIL_MESSAGE):
    
        try:
    
            # One second past the server's 10-year maximum record TTL.
    
            client.put(key, {"v": 1}, meta={"ttl": 315360001},
    
                       policy={"error_detail_verbosity": level})
    
            print(f"verbosity={level} unexpectedly succeeded")
    
        except ex.AerospikeError as e:
    
            print(f"verbosity={level} code={e.code} subcode={e.subcode} msg={e.msg}")
    ```
    
3.  Confirm the output matches this ladder. Accessor names differ by client; the values do not.
    
    | Level | Status | Subcode | Server message |
    | --- | --- | --- | --- |
    | `0` | `4` (parameter error) | `0` or absent | client text only, no server detail |
    | `1` | `4` | `1` (TTL invalid) | client text only |
    | `2` | `4` | `1` | contains `invalid record TTL 315360001` |
    
    Java calls these `getResultCode()`, `getSubCode()`, and `getMessage()`; Go uses `ResultCode` / `SubCode`; the Python SDK uses `result_code` / `sub_code` / `server_message`; Python legacy and Node.js use `code` / `subcode` / `msg` or `message`; C# uses `Result` / `SubCode` / `Message`; Rust uses `server_result_code()` / `sub_code()`; C reads `err.code` and `err.subcode`. The values are identical across all of them.
    
    Level `0` is the control. If the subcode is `0` at level `1`, or the server text is missing at level `2`, error details are not reaching your client: check the server version, the client version, and the cap from step 1.
    
4.  Optional. To verify verbosity `3`, write a record and reject it with a filter that reads one of its bins. See [Reading a trace](#reading-a-trace) for the same procedure in every client; the Java form is:
    
    ```java
    import com.aerospike.client.ExpressionTrace;
    
    import com.aerospike.client.exp.Exp;
    
    import com.aerospike.client.policy.Policy;
    
    AerospikeClient client = new AerospikeClient("CLUSTER_HOST", 3000);
    
    Key key = new Key("NAMESPACE_NAME", "SET_NAME", "RECORD_KEY");
    
    client.put(null, key, new Bin("age", 20));
    
    Policy policy = new Policy(client.readPolicyDefault);
    
    policy.errorDetailVerbosity = 3;
    
    policy.filterExp = Exp.build(Exp.gt(Exp.intBin("age"), Exp.val(21)));
    
    policy.failOnFilteredOut = true;
    
    try {
    
        client.get(policy, key);
    
    }
    
    catch (AerospikeException ae) {
    
        ExpressionTrace trace = ae.getExpressionTrace();
    
        System.out.println("status=" + ae.getResultCode()
    
            + " message=" + ae.getMessage());
    
        if (trace != null) {
    
            System.out.println(trace);
    
        }
    
    }
    
    finally {
    
        client.close();
    
    }
    ```
    
5.  Confirm `getResultCode()` is `27` (`ResultCode.FILTERED_OUT`) and the trace reports `op=gt`, `path=[gt]`, and `snippet=gt(bin_int("age"), 21)`.
    
    The filter must reference a bin. A filter over record metadata alone, such as `Exp.lastUpdate()`, is decided before the record is read and returns a message with no trace.
    

## Next steps

-   [Error subcodes](https://aerospike.com/docs/database/reference/error-subcodes), `(status, subcode)` catalog
-   [Error codes](https://aerospike.com/docs/database/reference/error-codes), top-level server and client codes
-   [Java client error handling](https://aerospike.com/docs/develop/client/java/error-handling#server-error-details)
-   [C client error handling](https://aerospike.com/docs/develop/client/c/error-handling#server-error-details)
-   [Go client error handling](https://aerospike.com/docs/develop/client/go/error-handling#server-error-details)
-   [Python client error handling](https://aerospike.com/docs/develop/client/python/error-handling#server-error-details)
-   [Node.js client error handling](https://aerospike.com/docs/develop/client/node/error-handling#server-error-details)
-   [C# client error handling](https://aerospike.com/docs/develop/client/csharp/error-handling#server-error-details)
-   [Rust client error handling](https://aerospike.com/docs/develop/client/rust/error-handling#server-error-details)
-   [`error-details-max-verbosity`](https://aerospike.com/docs/database/reference/config#service__error-details-max-verbosity), operator cap