This error occurs when a DynamoDB conditional write operation fails because the specified condition expression evaluated to false. It commonly happens during optimistic locking when the item has been modified by another process, or when custom conditional checks fail.
The ConditionalCheckFailedException is thrown by AWS SDK or CLI when a ConditionExpression you specified evaluates to false during a write operation (PutItem, UpdateItem, or DeleteItem). DynamoDB processes the condition before performing the write, and if the condition isn't met, it rejects the entire operation with this exception. This is an HTTP 400 client-side error, meaning the condition logic or expected values in your application code need to be corrected. The error protects data integrity by ensuring that writes only occur when specific conditions are satisfied, such as when an item hasn't been modified by another client (optimistic locking) or when specific attribute values match expected states. Unlike server-side errors, this exception indicates that your conditional logic is working as designed—it's preventing an operation that shouldn't proceed based on the current state of the data.
Set the ReturnValuesOnConditionCheckFailure parameter to ALL_OLD to see the actual item state that caused the condition to fail:
// AWS SDK v3 (JavaScript/TypeScript)
import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});
const command = new UpdateItemCommand({
TableName: "Users",
Key: { userId: { S: "user123" } },
UpdateExpression: "SET #status = :newStatus",
ConditionExpression: "#version = :expectedVersion",
ExpressionAttributeNames: {
"#status": "status",
"#version": "version"
},
ExpressionAttributeValues: {
":newStatus": { S: "active" },
":expectedVersion": { N: "5" }
},
ReturnValuesOnConditionCheckFailure: "ALL_OLD" // Returns item state on failure
});
try {
await client.send(command);
} catch (error) {
if (error.name === "ConditionalCheckFailedException") {
console.log("Current item state:", error.Item);
// Item will show actual version number and other attributes
}
}# AWS SDK (Python/boto3)
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Users')
try:
table.update_item(
Key={'userId': 'user123'},
UpdateExpression='SET #status = :newStatus',
ConditionExpression='#version = :expectedVersion',
ExpressionAttributeNames={
'#status': 'status',
'#version': 'version'
},
ExpressionAttributeValues={
':newStatus': 'active',
':expectedVersion': 5
},
ReturnValuesOnConditionCheckFailure='ALL_OLD'
)
except ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
print("Current item:", e.response.get('Item'))This reveals whether the version number is wrong, an attribute is missing, or values don't match expectations.
For optimistic locking scenarios, implement a retry mechanism that reads the latest item and reattempts the update:
async function updateWithRetry(userId, newStatus, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Read current item to get latest version
const getCommand = new GetItemCommand({
TableName: "Users",
Key: { userId: { S: userId } }
});
const { Item } = await client.send(getCommand);
const currentVersion = parseInt(Item.version.N);
// Attempt conditional update
const updateCommand = new UpdateItemCommand({
TableName: "Users",
Key: { userId: { S: userId } },
UpdateExpression: "SET #status = :newStatus, #version = :newVersion",
ConditionExpression: "#version = :expectedVersion",
ExpressionAttributeNames: {
"#status": "status",
"#version": "version"
},
ExpressionAttributeValues: {
":newStatus": { S: newStatus },
":expectedVersion": { N: currentVersion.toString() },
":newVersion": { N: (currentVersion + 1).toString() }
}
});
await client.send(updateCommand);
console.log(`Update succeeded on attempt ${attempt + 1}`);
return; // Success
} catch (error) {
if (error.name === "ConditionalCheckFailedException") {
if (attempt < maxRetries - 1) {
const backoffMs = Math.pow(2, attempt) * 100; // 100ms, 200ms, 400ms
console.log(`Retry ${attempt + 1} after ${backoffMs}ms`);
await new Promise(resolve => setTimeout(resolve, backoffMs));
} else {
throw new Error("Max retries exceeded");
}
} else {
throw error; // Different error type
}
}
}
}This pattern handles concurrent updates gracefully by refreshing the item state before each retry.
Double-check that your condition uses correct attribute names, reserved word substitutions, and comparison operators:
// WRONG - "status" is a reserved word, missing attribute name placeholder
const badCommand = new UpdateItemCommand({
ConditionExpression: "status = :val", // Will fail or behave unexpectedly
ExpressionAttributeValues: { ":val": { S: "active" } }
});
// CORRECT - Use ExpressionAttributeNames for reserved words
const goodCommand = new UpdateItemCommand({
ConditionExpression: "#status = :val",
ExpressionAttributeNames: { "#status": "status" },
ExpressionAttributeValues: { ":val": { S: "active" } }
});
// Check for attribute existence before updating
const safeUpdate = new UpdateItemCommand({
ConditionExpression: "attribute_exists(userId) AND #version = :expectedVersion",
ExpressionAttributeNames: { "#version": "version" },
ExpressionAttributeValues: { ":expectedVersion": { N: "3" } }
});
// Verify data types match (N for numbers, S for strings, etc.)
const typeCheck = new UpdateItemCommand({
ConditionExpression: "#count > :threshold",
ExpressionAttributeNames: { "#count": "loginCount" },
ExpressionAttributeValues: {
":threshold": { N: "10" } // Use N for number, not S
}
});Common mistakes include using reserved words without placeholders, wrong data types, or typos in attribute names.
When working with conditional writes, explicitly check for attribute existence to avoid unexpected failures:
# Ensure item exists before updating
table.update_item(
Key={'userId': 'user123'},
UpdateExpression='SET lastLogin = :timestamp',
ConditionExpression='attribute_exists(userId)', # Only update if item exists
ExpressionAttributeValues={':timestamp': '2025-01-15T10:30:00Z'}
)
# Prevent duplicate creation (put only if item doesn't exist)
table.put_item(
Item={
'userId': 'user456',
'email': '[email protected]',
'status': 'active',
'version': 1
},
ConditionExpression='attribute_not_exists(userId)' # Fail if already exists
)
# Combine existence check with value comparison
table.update_item(
Key={'orderId': 'order789'},
UpdateExpression='SET #status = :completed',
ConditionExpression='attribute_exists(orderId) AND #status = :pending',
ExpressionAttributeNames={
'#status': 'status'
},
ExpressionAttributeValues={
':pending': 'pending',
':completed': 'completed'
}
)These checks prevent operations on non-existent items or ensure items exist before complex updates.
Design your operations to be safe when retried, using conditional expressions to ensure idempotency:
// Idempotent increment - only add if not already processed
async function recordPayment(orderId, amount, transactionId) {
try {
await client.send(new UpdateItemCommand({
TableName: "Orders",
Key: { orderId: { S: orderId } },
UpdateExpression: "SET totalPaid = totalPaid + :amount, processedTransactions = list_append(if_not_exists(processedTransactions, :emptyList), :txnId)",
ConditionExpression: "NOT contains(processedTransactions, :txnId)",
ExpressionAttributeValues: {
":amount": { N: amount.toString() },
":txnId": { L: [{ S: transactionId }] },
":emptyList": { L: [] }
}
}));
return { success: true };
} catch (error) {
if (error.name === "ConditionalCheckFailedException") {
// Transaction already processed - this is OK for idempotency
console.log(`Transaction ${transactionId} already processed`);
return { success: true, alreadyProcessed: true };
}
throw error;
}
}
// State machine pattern - only allow valid state transitions
async function updateOrderStatus(orderId, newStatus, currentStatus) {
const command = new UpdateItemCommand({
TableName: "Orders",
Key: { orderId: { S: orderId } },
UpdateExpression: "SET #status = :newStatus, updatedAt = :timestamp",
ConditionExpression: "#status = :currentStatus",
ExpressionAttributeNames: { "#status": "status" },
ExpressionAttributeValues: {
":newStatus": { S: newStatus },
":currentStatus": { S: currentStatus },
":timestamp": { S: new Date().toISOString() }
}
});
await client.send(command);
}This approach treats ConditionalCheckFailedException as a valid outcome in certain scenarios.
High-Concurrency Patterns:
In systems with heavy concurrent writes, consider using DynamoDB Streams to trigger downstream processes instead of blocking on conditional writes. You can also implement distributed locking with DynamoDB's conditional writes combined with TTL attributes to create temporary locks.
For scenarios requiring strong consistency guarantees across multiple items, use DynamoDB Transactions (TransactWriteItems), which provide atomic all-or-nothing operations across up to 100 items. Transactions also support conditional checks but throw TransactionCanceledException instead.
Performance Considerations:
ConditionalCheckFailedException consumes read capacity even though the write fails. Each failed conditional write uses one write capacity unit for the attempt plus read capacity for the condition evaluation. In high-failure scenarios, this can significantly impact throughput and costs.
DynamoDBMapper and ORM Support:
When using AWS SDK's DynamoDBMapper (Java), Object Persistence Model (.NET), or third-party ORMs, optimistic locking is often handled automatically via version attributes. The library increments version numbers and adds conditional checks transparently. If you encounter ConditionalCheckFailedException in this context, it means the item was modified between your read and write operations—refresh the object and retry.
Testing Conditions:
Use the ReturnValuesOnConditionCheckFailure parameter extensively during development to understand exactly why conditions fail. In production, log the returned item state to monitoring systems to identify patterns in failed conditions, such as frequently conflicting version numbers indicating hotspot contention on specific items.
ImportConflictException: There was a conflict when attempting to import to the table
How to fix 'ImportConflictException: There was a conflict when attempting to import to the table' in DynamoDB
ResourceNotFoundException: Requested resource not found
How to fix "ResourceNotFoundException: Requested resource not found" in DynamoDB
TrimmedDataAccessException: The requested data has been trimmed
How to fix "TrimmedDataAccessException: The requested data has been trimmed" in DynamoDB Streams
GlobalTableNotFoundException: Global Table not found
How to fix "GlobalTableNotFoundException: Global Table not found" in DynamoDB
InvalidExportTimeException: The specified ExportTime is outside of the point in time recovery window
How to fix "InvalidExportTimeException: The specified ExportTime is outside of the point in time recovery window" in DynamoDB