DynamoDB ConditionalCheckFailedException Explained
ConditionalCheckFailedException is DynamoDB telling you a condition wasn't met — often by design. When it's expected, when it's a bug, and how to handle it correctly.
On this page
- When it’s expected (and you should just handle it)
- Create-if-not-exists
- Optimistic locking
- When it’s a real bug
- The condition doesn’t match the data you think is there
- attribute_exists / attribute_not_exists on the wrong attribute
- Transactions: one failed condition fails everything
- Two things people miss
- Handle it, don’t fear it
Unlike most DynamoDB exceptions, ConditionalCheckFailedException is frequently not a bug — it’s DynamoDB correctly telling you that a condition you attached to a write wasn’t satisfied:
ConditionalCheckFailedException: The conditional request failed
Whether that’s a problem depends entirely on why you wrote the condition. Let’s separate the expected cases from the real bugs.
When it’s expected (and you should just handle it)
Create-if-not-exists
The classic pattern: write an item only if it doesn’t already exist, using attribute_not_exists:
new PutItemCommand({
TableName: "Users",
Item: { pk: { S: "USER#42" }, email: { S: "a@b.com" } },
ConditionExpression: "attribute_not_exists(pk)"
});
If a user with that key already exists, the write fails with ConditionalCheckFailedException — by design. That’s your “already registered” branch, not an outage. Catch it:
try {
await client.send(putCommand);
} catch (err) {
if (err.name === "ConditionalCheckFailedException") {
return { alreadyExists: true }; // expected business branch
}
throw err; // anything else is a real error
}
Optimistic locking
To avoid lost updates under concurrency, guard writes with a version attribute:
new UpdateItemCommand({
TableName: "Orders",
Key: { pk: { S: "ORDER#1001" } },
UpdateExpression: "SET #s = :new, version = :next",
ConditionExpression: "version = :expected",
ExpressionAttributeNames: { "#s": "status" },
ExpressionAttributeValues: {
":new": { S: "shipped" }, ":next": { N: "8" }, ":expected": { N: "7" }
}
});
A failure here means another writer changed the item first. The correct response is usually read the latest item and retry, not crash.
When it’s a real bug
The condition doesn’t match the data you think is there
If a write should succeed but keeps failing the check, your assumption about the item’s current state is wrong. Read the item and compare it against the condition:
- Is the attribute you’re testing actually present?
attribute_exists(x)fails ifxwas never set. - Is the value the type you expect?
version = :expectedwith:expectedas a String won’t match a Number-typedversion. - Did a previous partial write leave the item in an unexpected shape?
attribute_exists / attribute_not_exists on the wrong attribute
attribute_not_exists(pk) tests the partition key to detect a brand-new item. Testing a non-key attribute that’s sometimes absent will make the condition pass or fail unpredictably. For “is this a new item?”, always test a key attribute.
Transactions: one failed condition fails everything
In a TransactWriteItems call, if any item’s condition fails, the entire transaction is cancelled with a TransactionCanceledException whose CancellationReasons include ConditionalCheckFailed. Inspect the reasons array to find which item failed:
TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed, None]
The first slot maps to the first item in the transaction. See Transactions for the full model.
Two things people miss
- Failed conditions still cost capacity. A rejected conditional write consumes write capacity units. High-volume conditional writes on a hot key add up.
- Get the old values back. Set
ReturnValuesOnConditionCheckFailure: "ALL_OLD"on the request to receive the item’s current attributes in the exception — invaluable for debugging why the condition failed without a separate read.
Handle it, don’t fear it
The right mental model: a condition expression is a rule, and ConditionalCheckFailedException is DynamoDB enforcing it. Decide up front whether a given failure is an expected branch (catch and handle) or an invariant violation (read, reconcile, retry).
Tablyne lets you inspect an item’s exact current attributes and types, and build conditional writes visually — so when a condition fails, you can see the real state of the item immediately instead of blind-debugging. Download Tablyne free.
Frequently asked questions
Is ConditionalCheckFailedException an error I should fix?
Often it's not an error at all — it's the expected outcome of a condition you wrote. For example, a create-if-not-exists that fails because the item already exists is working as designed. Catch it and handle it as a normal branch, not a failure.
How do I prevent overwriting an existing item?
Add a ConditionExpression of attribute_not_exists(pk) to your PutItem. If an item with that key already exists, the write is rejected with ConditionalCheckFailedException instead of silently overwriting.
Does a failed condition still consume capacity?
Yes. A conditional write that fails the check still consumes write capacity units. This matters if you rely heavily on conditional writes in a hot path.