DEV Community

Cover image for DynamoDB Conditional Write with the AWS CLI
DynoTable
DynoTable

Posted on Originally published at dynotable.com

DynamoDB Conditional Write with the AWS CLI

A conditional write is straightforward to send from the shell and awkward to read, because the interesting result of a failed one arrives as an error rather than as output. DynamoDB condition expressions covers what the expression can say; this page is about running one from the CLI and getting the losing item back out of the failure.

Code

aws dynamodb update-item \
  --table-name 'Music' \
  --key '{"Artist":{"S":"Arturo Sandoval"},"SongTitle":{"S":"Cubano Chant"}}' \
  --update-expression 'SET #upd0 = :updValue0, #version = :newVersion' \
  --condition-expression 'attribute_exists(#cond0) AND #version = :expectedVersion' \
  --expression-attribute-names '{"#upd0":"Genre","#version":"Version","#cond0":"Artist"}' \
  --expression-attribute-values '{":updValue0":{"S":"Latin Jazz"},":expectedVersion":{"N":"7"},":newVersion":{"N":"8"}}'
Enter fullscreen mode Exit fullscreen mode

On success the command prints nothing and exits 0. If another writer got there first, the condition fails and the CLI reports the service message:

An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation:
The conditional request failed
Enter fullscreen mode Exit fullscreen mode

Explanation

  • Success — silent. No output, exit 0. There is nothing to parse and nothing to assert on, so a shell script has to treat the exit status as the result. Add --return-values ALL_NEW if you want the updated item printed.
  • Failure is exit status 254, which is the CLI v2 code for a client-side error and is shared with a malformed request. Branch on the message before you retry, or a typo in your expression becomes an infinite backoff loop.
  • --return-values-on-condition-check-failure ALL_OLD does work here. Valid values are ALL_OLD and NONE, and it consumes no read capacity. Getting the item out of the error takes one more flag, covered below.
  • Shared placeholder namespace — the condition and the update are separate flags, yet --expression-attribute-names and --expression-attribute-values are merged across --update-expression and --condition-expression, which is why the generated names run #upd0, #cond0 rather than restarting per clause. Reuse a placeholder for two different meanings and the second silently wins.
  • A failed write is still billed. The Developer Guide is explicit: a condition evaluating to false consumes write capacity anyway, sized on the larger of the old and new item. Conditions are not a cheap existence probe.

The failure output, and how to get the item out of it

Run the fence once and it succeeds silently. Run it a second time, when Version is no longer 7, and aws-cli/2.36.9 prints to stderr:

aws: [ERROR]: An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation: The conditional request failed
Enter fullscreen mode Exit fullscreen mode

Add --return-values-on-condition-check-failure ALL_OLD and the default output tells you there is more, without showing it:

aws: [ERROR]: An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation: The conditional request failed

Additional error details:
Item: <complex value>
Use "--cli-error-format json" or another error format to see the full details.
Enter fullscreen mode Exit fullscreen mode

<complex value> is the item, withheld by the default text renderer. Add --cli-error-format json and the whole thing prints:

{
    "Message": "The conditional request failed",
    "Code": "ConditionalCheckFailedException",
    "Item": {
        "Artist": {"S": "Arturo Sandoval"},
        "Year": {"N": "1994"},
        "Version": {"N": "8"},
        "SongTitle": {"S": "Cubano Chant"},
        "AlbumTitle": {"S": "Danzon"},
        "Genre": {"S": "Latin Jazz"}
    }
}
Enter fullscreen mode Exit fullscreen mode

(Attribute maps folded onto one line each; everything else is as printed.) Version is 8 and Genre is set because the first run succeeded. That is the optimistic-locking loop closed from a shell script: pipe stderr through jq -r '.Item.Version.N', feed it back as :expectedVersion, retry. No get-item, and no window between the read and the retry for a third writer to slip into.

The retries are not free. Each rejected attempt consumes a write unit, so a contended key under a tight loop bills steadily while making no progress. The pricing calculator turns a write rate into a monthly figure if you want to know what a retry storm actually costs before you cap the attempts.

To run these guards against your own tables without shell-quoting the placeholder maps, download DynoTable.

Related examples

References

Last verified 2026-07-28 against the official AWS documentation linked above.

Top comments (0)