---
title: "AEL control structures and postfix flags"
description: "AEL reference: the when conditional, let variable binding with common gotchas, and the full postfix flag reference including :NO_FAIL semantics."
---

# AEL control structures and postfix flags

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

Reference page: part of the [AEL reference](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference). Covers `when`/`let` control structures and the postfix flags that modify write and read terminals. See [Applies to](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference#applies-to) on the overview page for SDK and Database version requirements.

## Conditional: `when`

`when` works like an `IF … THEN … ELSIF … ELSE` chain in other languages: it evaluates each condition in order and returns the value paired with the first one that’s true.

```plaintext
when (cond1 => val1, cond2 => val2, default => val3)
```

| Part | Requirement |
| --- | --- |
| Conditions (`condN`) | `TRILEAN` |
| Actions (`valN`) | All branches the same type, except the reserved literals `unknown` and `error` (explained in the next paragraph) |
| `default => …` | Required fallback branch |

Result type is the unified action type of all non-`unknown` / non-`error` branches.

```plaintext
when ($.tier == 1 => 'gold', $.tier == 2 => 'silver', default => 'bronze')
```

Reserved trilean value literals: the words `unknown` and `error` are value literals, not exceptions, parse failures, or shorthand for “throw”. Both spellings are synonymous: they may appear on any branch regardless of the types on other branches, and at evaluation time both produce the `TRILEAN` value `unknown`. Typical use: `default => unknown` (or `default => error`) when no condition matches and the expression should yield an indeterminate result rather than a typed default. This is distinct from a condition or comparison returning `unknown` because a bin or path operand is absent (see [TRILEAN](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference#trilean-three-valued-logic)).

```js
/* Parse error — branch result types must agree (INT default vs STRING arms) */

when ($.tier == 1 => 'gold',

      $.tier == 2 => 'silver',

      $.tier == 3 => 'bronze',

      default => 0

)

/* Valid — `unknown` and `error` are interchangeable value literals */

when ($.tier == 1 => 'gold',

      $.tier == 2 => 'silver',

      $.tier == 3 => 'bronze',

      default => unknown   /* same runtime value as `default => error` */

)
```

## Variable binding: `let`, `then`

```plaintext
let (var1 = expr1, var2 = expr2) then (bodyExpr)
```

References to bound variables use `${varName}`. Variable types are inferred from their initializer expressions. Variables can reference earlier variables in the same binding list:

```plaintext
let (total = $.price * $.qty, discount = ${total} / 10) then (${total} - ${discount} > 400)
```

### Float literals in `let` expressions cause a parse error.

::: caution
Combining a float literal (such as `0.1`, `1.5`) with an `INT` value in the same `let` arithmetic is a compile-time type error. See [Type consistency in `let` bindings](#type-consistency-in-let-bindings) for more information on mixing bins whose types the compiler can only infer from context.
:::

Use only integer arithmetic in `let` expressions unless all operands are explicitly floats:

```plaintext
/* Parse error (cannot compare: FLOAT vs INT): INT combined with a float literal */

let (total = $.price * $.qty, tax = ${total} * 0.1) then (${total} + ${tax} > 900)

/* Parse error (type mismatch: INT vs FLOAT): INT bin multiplied by a float literal */

$.intBin:INT * 0.1 > 5

/* Fixed: keep everything INT (scale by 1000 or use integer percentages) */

let (total = $.price * $.qty, tax_pct = 10, tax = ${total} / ${tax_pct}) then (${total} + ${tax} > 990)
```

### Type consistency in `let` bindings

::: caution
All arithmetic within a `let` expression must use consistent types. Mixing `INT` and `FLOAT` values that the compiler can only infer from context, rather than reject as a static mismatch, behaves differently depending on where the expression runs. In a filter expression (including XDR filters), there’s no parse error: the mismatch evaluates to `unknown`, silently excluding matching records, so a production filter can exclude every record without any indication of the mismatch. In a read or write expression, the same mismatch is returned to the client as a runtime error instead of failing silently.
:::

To avoid this:

-   Pin every bin used in `let` arithmetic to an explicit type (`:INT` or `:FLOAT`), or
-   Use `.toFloat()` / `.toInt()` explicitly to cast before combining.

`(${name}).toFloat()` / `(${name}).toInt()` follow the same explicit-cast rules as casting a bin path (see [Types and type suffixes](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference#types-and-type-suffixes)). A `let` variable reference cannot have functions called on it, so it must be parenthesized before calling a cast method: `${total}.toFloat()` is a syntax error, but `(${total}).toFloat()` compiles.

```plaintext
/* All INT arithmetic: */

let (total = $.price * $.qty, discount = ${total} / 10) then (${total} - ${discount} > 400)

/* Mixed types (NOTE: INT * FLOAT silently excludes every record in a filter expression, but is a runtime error in a read/write expression, if types don't align): */

let (discount_rate = 0.13, total = $.price * $.qty, discount = ${total} * $.discount_rate) then (${total} - ${discount} > 400)

/* Fixed with explicit casts and pinned types: */

let (total = $.price:INT * $.qty, discount = (${total}).toFloat() * $.discount_rate:FLOAT) then ((${total}).toFloat() - ${discount} > 400.0)
```

## Postfix flags

Attach postfix flags immediately after `)` on path terminals, not as named parameters inside `()`:

| Flag | Valid on | Description |
| --- | --- | --- |
| `:NO_FAIL` | Collection data type (CDT) writes; `modify()`, `remove()`; pathed string modify; `hllInit`, `hllAdd`; all BLOB bit modify ops | Absent-path / policy tolerance — see [`:NO_FAIL` semantics](#no_fail-semantics) |
| `:PARTIAL` | Map `putItems`, `insertItems`, `updateItems`; list `appendItems`, `insertItems` (index path); BLOB `bitRemove`, `bitSet`, `bitOr`, `bitXor`, `bitAnd`, `bitNot`, `bitLshift`, `bitRshift` | On bulk CDT writes: apply entries that succeed even when others fail; implies `:NO_FAIL`. On BLOB bit ops: clip the op to the blob end when the range extends past the end; does not imply `:NO_FAIL` |
| `:CREATE_ONLY` | `hllInit`, `hllAdd`; `bitResize`, `bitInsert` | Fail if the operation would modify an existing bin |
| `:UPDATE_ONLY` | `hllInit`; all BLOB bit modify ops | Fail if the operation would create a new bin. Parse error on `hllAdd` |
| `:ADD_UNIQUE` | List `append`, `appendItems`, `insert`, `insertItems`, `setTo`, `add` | Fail (or skip under `:NO_FAIL`) when an element equals one already in the list. A duplicate within the same bulk payload reports `OP_NOT_APPLICABLE`; a duplicate against existing list content reports `ELEMENT_EXISTS` |
| `:DROP_DUPS` | `sort()` | Drop duplicate elements while sorting |
| `:REVERSE` | `getIndexes()`, `getRanks()` | Reverse index/rank direction |
| `:UNORDERED` | `getMaps()` | Unordered return map shape on `getMaps()` only — distinct from the map literal and path-segment create-order uses of `:UNORDERED`; see [Literals](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference#literals) |
| `:PERSIST_INDEX` | Bin root only | Persist top-level map index on create |
| `:UNSORTED_PAD` | Path segment that materializes a missing list — not valid on `modify()` or `remove()` paths | Create an unsorted list with elements allowed to be inserted anywhere past the end of the list when materializing a missing container; see [Bounded list writes](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference/paths#bounded-list-writes-default) |

Terminal-kind rule (create-order vs. `:NO_FAIL` vs. reads): [create-order suffixes](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference/paths#collection-create-order-suffixes) are write-only and allowed only on write terminals that can create containers. `:NO_FAIL` is write-only. Read terminals take neither create-order suffixes nor `:NO_FAIL`. `:UNSORTED_PAD` is a create-order suffix.

Read paths when a segment is absent: reads cannot use `:NO_FAIL`: a missing CDT context step fails under strict navigation (see [Record and bin prefix](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference/paths#record-and-bin-prefix)). Use `exists()` to test presence instead — it returns `false` when the path does not match.

```plaintext
/* :REVERSE reverses the index/rank direction of the returned list */

$.scores:LIST.[1:3].getIndexes():REVERSE

/* :PERSIST_INDEX persists the top-level map's key index so subsequent

   key-based lookups on this bin can use it, instead of rebuilding it each time.

   It's valid on the bin root only, so it must come before any path segments. */

$.m:KEY_ORDERED:PERSIST_INDEX.k.setTo(1)
```

```plaintext
$.optional.field.setTo('value'):NO_FAIL

$.tags.append('x'):ADD_UNIQUE

$.tags.sort():DROP_DUPS

$.m.{@a: d}.getMaps():UNORDERED

$.m:MAP.insertItems({a: 1, b: 2}):PARTIAL

$.h.hllInit(indexBits: 14):CREATE_ONLY
```

`:PARTIAL` trades all-or-nothing atomicity for tolerance of individual failures within a bulk write. For example, consider using a list as a set with `:ADD_UNIQUE`. To add several candidate elements where some may already be present, `$.tags.appendItems(candidates):ADD_UNIQUE:PARTIAL` inserts only the elements that aren’t already in the list and skips the duplicates, instead of failing the whole call on the first one it finds. Without `:PARTIAL` or `:NO_FAIL, a bulk write fails atomically on the first error, which is the right choice when your application requires all-or-nothing semantics.` :NO\_FAIL`is implied by`:PARTIAL\` in this context.

### `:NO_FAIL` semantics

::: caution
`:NO_FAIL` converts a write failure into a no-op that reports success: the call returns successfully, but the bin is left unchanged and nothing indicates the write didn’t happen. Apply `:NO_FAIL` only when that record subset is deliberately expected or tolerated — for example, when writing to records with a slightly different schema is a known, acceptable case — and consider auditing on the application side (for example checking the write result or `generation`) when correctness matters.
:::

The flag has two runtime axes; both are narrower than “suppress any failure”:

| Axis | Valid on | Effect when set |
| --- | --- | --- |
| **Path-level (absent CTX)** | CDT writes; `modify()`, `remove()`; pathed string modify | A CDT context segment on the compiled path is missing in the bin → no-op; the original bin is left unchanged |
| **Bin-level (create/update policy)** | `hllInit`, `hllAdd`; BLOB bit modify ops | A `:CREATE_ONLY` / `:UPDATE_ONLY` (or related type) conflict on the bin → no-op instead of failing |

`:NO_FAIL` does not:

-   Suppress parse errors (including invalid flag placement on a terminal — AEL rejects nonsensical postfix at compile time).
-   Suppress read-terminal failures.
-   Suppress op-specific failures unless a separate mechanism applies (for example BLOB `:PARTIAL` clips a range).

::: caution
Without `:PARTIAL`, a bulk write fails the entire call atomically on the first per-key or per-element error — this is the default, and it’s the right choice when your application requires all-or-nothing semantics. If you want individual failures tolerated instead, add `:PARTIAL` explicitly.
:::

`:PARTIAL` and `:NO_FAIL`: on bulk CDT ops (`putItems`, `insertItems`, `updateItems`, `appendItems`, list `insertItems`), `:PARTIAL` requires tolerant failure and automatically applies `:NO_FAIL` at AEL compile time. Without `:PARTIAL`, bulk CDT ops fail atomically (one failed entry aborts the entire call). On BLOB bit modify ops, `:PARTIAL` and `:NO_FAIL` are independent: `:PARTIAL` clips to the blob end, and `:NO_FAIL` suppresses create/update flag conflicts. Write both on BLOB when needed, for example `bitSet(…):PARTIAL:NO_FAIL`. On bulk CDT, explicit `:NO_FAIL` with `:PARTIAL` is redundant.

`:NO_FAIL` restrictions, summarized:

-   Valid only on writes, never on read terminals.
-   Applies to: absent-context container creation on a write path (such as the earlier `$.optional.field` example, where `optional` doesn’t yet exist), `BLOB`/`HLL` bin create/update policy, and pathed `STRING` modify functions (not a bare bin or `(expr)` receiver).
-   Does not mean “suppress any failure” — it tolerates one specific failure condition for its context.

The Developer SDK splits `:NO_FAIL` across the same two axes, as separate builder options on operation-expression builders: the path-level axis (absent CDT context, an expression resolving to `unknown` or a non-bin type) maps to `ignoreEvalFailure()` / `ignore_eval_failure=True`, and the bin-level axis (a `:CREATE_ONLY` / `:UPDATE_ONLY` create/update policy conflict) maps to `ignoreOpFailure()` / `ignore_op_failure=True`. The two are independent and can be combined on the same operation, for example `opt -> opt.ignoreOpFailure().ignoreEvalFailure()` in Java.

## Next steps

-   [Type inference and limits](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference/type-inference-and-limits) — compile limits and name-collision disambiguation
-   [String, BLOB, and HLL functions](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference/type-functions) — per-op flag validity tables for BLOB and HLL
-   [Functions and terminals](https://aerospike.com/docs/develop/client/sdk/concepts/ael/reference/functions) — path write terminals that accept these flags