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"}}'

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

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

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.

<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"}
    }
}

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

References

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

Work with DynamoDB without the Console

A fast DynamoDB desktop client that runs the real SQL DynamoDB can’t — JOINs, GROUP BY, aggregates — with visual editing and an AI agent on your own Bedrock keys.

Free 30-day trial, no credit card — then the Free plan with no time limit.