Skip to content

Server error details

For the complete documentation index see: 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. 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. 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.
  • The operation paths listed in 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)

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

The Developer SDKs use typed exceptions and recovery hints appropriate to their language. See Error handling.

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.

LevelServer returns
0Top-level status only (default)
1Numeric subcode
2Subcode and human-readable message
3Subcode, 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 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.

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.

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.

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().

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.

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.

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);
}
}

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

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:

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.

Coverage

Database 8.2.0 returns error details for these areas:

AreaWhat you get
Single-record read, write, operate, deleteSubcodes and messages for TTL validation, set limits, truncation, and more. Includes proxied commands: details follow the response back to your client.
BatchEach record in the batch carries its own error detail. A batch where three records fail returns three independent subcode/message pairs.
ExpressionsFailures in filter expressions and expression operations return messages and, at verbosity 3, an expression trace.
Collection data type (CDT) List/MapIndex/rank out of bounds, bounded-list overflow, per-operation messages
HLLIndex bits unset, fold/minhash mismatches
BitsOffset/size out of range, resize exceeded
String operationsInvalid parameters, regular expressions, UTF-8, and conversion failures
TransactionsRecord locked, transaction ID mismatch, expiry details
User-defined function (UDF), client-invokedDefinition, 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:

    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:

    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());
    }
    }
  3. Confirm the output matches this ladder. Accessor names differ by client; the values do not.

    LevelStatusSubcodeServer message
    04 (parameter error)0 or absentclient text only, no server detail
    141 (TTL invalid)client text only
    241contains 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 for the same procedure in every client; the Java form is:

    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