What it means
A conditional write asks DynamoDB to apply a change only if a statement about the current item is true, and to evaluate that statement atomically with the write itself. When the statement is false, DynamoDB does not write and raises ConditionalCheckFailedException.
Nothing has gone wrong. This is the feature working. The exception is how a conditional write reports "the condition you gave me was not satisfied", and it is the entire point of asking for the condition in the first place. The atomicity is what makes it valuable: no separate read can tell you whether an item exists and then safely act on that answer, because another writer can act in between. The condition and the write happen together or not at all.
Where it becomes a real problem is when code treats it as an infrastructure failure. An idempotent insert guarded by attribute_not_exists(pk) will fail this way every single time a message is redelivered — and SQS, EventBridge and Lambda's own async retries all deliver at least once, so duplicates are normal rather than exceptional. If that exception propagates out of the handler, the invocation fails, the event is retried, the retry fails identically, and eventually a message that was processed correctly the first time lands in a dead-letter queue.
That is why the frequency matters more than the occurrence. A low, steady rate under concurrency is optimistic locking working: writers contending, one winning, the loser re-reading and trying again. A sudden jump, or a rate close to every attempt, means the condition has stopped describing reality — an attribute renamed, a type changed, a key format migrated — and every write is now being rejected for a reason that has nothing to do with contention. Seeing which invocations carried a retry, and what the item looked like when the check failed, is what separates the two.
CloudWatch’s Errors metric: Whether CloudWatch’s Errors metric counts this depends on whether the error escaped your handler, which the log line alone does not settle. This is usually not a fault at all — it is the mechanism working. Code that catches it and branches is not counted; code that lets it escape fails the invocation and is. LogStitch reports the example below as caught, which for this error is very often the correct reading.
What it looks like in CloudWatch
This is the shape the failure arrives in: the lines of one invocation scattered among everything else the log group received at the same moment.
CloudWatch Logs Before
8 raw lines, in the order CloudWatch delivered them.
LogStitch After
The same lines, grouped into the invocation they belong to.
Error2c9e4a71-0f83-4b56-8d19-7a3c6e0b5f24
- 15:11:02.114PLAT
START RequestId: 2c9e4a71-0f83-4b56-8d19-7a3c6e0b5f24 Version: $LATEST
- 15:11:02.118INFO
INFO Reserving inventory sku=SKU-4417 qty=2
- 15:11:02.244DEBUG
DEBUG Conditional put version=7
- 15:11:02.288ERROR
ERROR ConditionalCheckFailedException: The conditional request failed at throwDefaultError (/var/task/node_modules/@smithy/smithy-client/dist-cjs/index.js:867:20) at reserve (/var/task/src/inventory.js:51:9)3 lines - 15:11:02.402PLAT
END RequestId: 2c9e4a71-0f83-4b56-8d19-7a3c6e0b5f24
- 15:11:02.402PLAT
REPORT RequestId: 2c9e4a71-0f83-4b56-8d19-7a3c6e0b5f24 Duration: 288.11 ms Billed Duration: 289 ms Memory Size: 512 MB Max Memory Used: 102 MB Status: error
The panel on the right is generated by running the excerpt on the left through the same parser that powers the free web stitcher — it is what the tool actually produces for this input, not an illustration of it.
How to confirm it from the logs
Read the condition expression in the code that raised it, then work out which branch it guarded. attribute_not_exists(pk) failing means the item already exists. A version check failing means someone else wrote first. A status check failing means the record was not in the state you required.
The frequency tells you whether this is a problem. A steady low rate under concurrency is the expected shape of optimistic locking — contention happening and being handled. A sudden rise, or a rate near 100%, means the condition no longer matches reality: a schema change, a backfill, or a key format that moved.
Causes, most likely first
The item already exists and the write required that it not
Look for attribute_not_exists on the partition key, which is the standard idempotent-insert guard. Failing means a record with that key is already there — frequently a retry of a request that already succeeded, in which case the condition is doing exactly what it was added for.
Optimistic locking lost a race
Look for a version or updatedAt comparison in the condition. Under concurrency two writers read the same version, both try to write, and one loses. This is the intended behaviour of optimistic locking, and the correct response is to re-read and retry rather than to fail.
The record is not in the state the condition requires
Read the item as it exists now and compare it against the condition. A state-machine guard like status = :pending fails when something already advanced the record, which is a real business outcome rather than an infrastructure problem.
The condition references an attribute that is absent or differently typed
Check whether the attribute exists on the item at all, and with the type the comparison expects. A condition on a field that was renamed, or a numeric comparison against a value stored as a string, fails for every item — producing a near-100% rate rather than a contention-shaped one.
Fixes
Catch it and branch, rather than letting it fail the invocation
This exception is a return value in disguise. Handling it explicitly turns a logged error and a failed invocation into ordinary control flow, and stops it polluting your error rate with events that are working as designed.
jsimport { ConditionalCheckFailedException } from "@aws-sdk/client-dynamodb";
try {
await client.send(new PutItemCommand({
TableName: "orders",
Item: item,
ConditionExpression: "attribute_not_exists(pk)"
}));
return { created: true };
} catch (error) {
if (error instanceof ConditionalCheckFailedException) {
// Already there — this request is a duplicate, which is fine.
return { created: false, duplicate: true };
}
throw error;
}
Retry an optimistic-locking failure by re-reading first
Retrying the same write with the same stale version fails identically every time. The retry has to re-read the current item and recompute, which is what makes the loop converge.
jsasync function updateWithRetry(id, mutate, attempts = 5) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
const current = await getItem(id);
try {
return await putItem(mutate(current), {
ConditionExpression: "version = :v",
ExpressionAttributeValues: { ":v": current.version }
});
} catch (error) {
if (error.name !== "ConditionalCheckFailedException") throw error;
// Someone else wrote; loop re-reads and recomputes.
}
}
throw new Error(`Could not update ${id} after ${attempts} attempts`);
}
Ask DynamoDB to return the item that failed the check
ReturnValuesOnConditionCheckFailure includes the current item in the exception, so you can log what the record actually looked like instead of issuing another read to find out.
jsawait client.send(new PutItemCommand({
TableName: "orders",
Item: item,
ConditionExpression: "attribute_not_exists(pk)",
ReturnValuesOnConditionCheckFailure: "ALL_OLD"
}));
// On failure, error.Item holds the record that caused the condition to fail.
Also seen as
The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.
Related errors
Errors that show up alongside this one, or that people mistake for it.
References
- Amazon DynamoDB Developer Guide — Condition expressions
- Amazon DynamoDB Developer Guide — Optimistic locking with version number
LogStitch finds this automatically, across every invocation in your account.
Paste a log excerpt into the free web stitcher and see it grouped, classified, and measured in your browser — nothing is uploaded. Or run the Mac app against your own AWS profiles and get the same view over every function you own.