DynamoDB Conditional Write in Node.js (AWS SDK v3)
The interesting part of a conditional write in AWS SDK v3 is not the ConditionExpression, which works the same everywhere and is covered in DynamoDB condition expressions. It is the failure path: v3 hands you the losing item on the thrown error, if you asked for it, and gives you nothing if you did not.
Code
import {DynamoDBClient, UpdateItemCommand} from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({region: 'us-east-1'});
// Update the item only if nobody changed it since we read version 7.
const command = new UpdateItemCommand({
TableName: 'Music',
Key: {
Artist: {S: 'Arturo Sandoval'},
SongTitle: {S: 'Cubano Chant'}
},
UpdateExpression: 'SET #upd0 = :updValue0, #version = :newVersion',
ConditionExpression: 'attribute_exists(#cond0) AND #version = :expectedVersion',
ExpressionAttributeNames: {
'#upd0': 'Genre',
'#version': 'Version',
'#cond0': 'Artist'
},
ExpressionAttributeValues: {
':updValue0': {S: 'Latin Jazz'},
':expectedVersion': {N: '7'},
':newVersion': {N: '8'}
},
ReturnValuesOnConditionCheckFailure: 'ALL_OLD'
});
try {
await client.send(command);
console.log('Updated to version 8');
} catch (err) {
if (err.name === 'ConditionalCheckFailedException') {
// With ReturnValuesOnConditionCheckFailure: 'ALL_OLD', the current item
// rides back on the exception — no extra read to see what beat you.
console.log('Lost the race — item is now:', err.Item);
} else {
throw err;
}
}Explanation
- The failed check is a thrown error, not a status field. v3 rejects the promise, so the write path and the lost-race path are different branches.
err.name === 'ConditionalCheckFailedException'is the discriminator; anything else has to be re-thrown, which is what theelsein the fence is for. Swallow the wholecatchand you have silently turned a throttle into a no-op. ReturnValuesOnConditionCheckFailureis the only way to see who beat you. Without it, the error carries the message and nothing else, and you are back to aGetItemyou did not need. The API reference sets its valid values asALL_OLD | NONEand confirms it consumes no read capacity.err.Itemis a rawAttributeValuemap, the same shape as theKeyyou sent, not plain JavaScript. Run it throughunmarshallfrom@aws-sdk/util-dynamodbbefore you compareVersionto a number, or you will be comparing against{N: '9'}.- The failed write is still billed. The Developer Guide is explicit that a condition evaluating to false still consumes write capacity, sized on the larger of the old and new item. A retry loop on a hot key is a real line on the bill, so cap the attempts.
- Every name in the fence is aliased (
#version→Version,#cond0→Artist) because the Expression Builder that generated it aliases unconditionally. That is heavier than necessary here and never wrong, which is the trade it makes.
Reading the loser's copy off the exception
Set the stored Version to 9 and run the fence, which expects 7. DynamoDB Local 3.3.0 throws, and the caught error carries:
err.name ConditionalCheckFailedException
err.message The conditional request failed
err.$metadata.httpStatusCode 400
err.Item {
Artist: { S: 'Arturo Sandoval' },
Year: { N: '1994' },
Version: { N: '9' },
SongTitle: { S: 'Cubano Chant' },
AlbumTitle: { S: 'Danzon' }
}That Version: 9 is the whole point. The retry can go straight back through the update with :expectedVersion set to 9, with no extra read and no window in which a third writer slips in between your GetItem and your retry.
Delete ReturnValuesOnConditionCheckFailure from the same command and re-run it. Same name, same message, same 400, and err.Item is undefined. Nothing warns you: the parameter is optional, its absence is not an error, and the code that reads err.Item just starts logging undefined in production.
Note also that a 400 here does not mean a malformed request. ValidationException and ConditionalCheckFailedException share the status code, and only one of them is a bug, which is why the branch is on err.name and never on the status.
To watch a condition succeed and fail against your own data, with the expression written for you rather than typed, download DynoTable.
Related examples
- DynamoDB conditional write in Python — the same optimistic lock with boto3.
- DynamoDB conditional write with the AWS CLI — the same optimistic lock from the shell.
- DynamoDB PutItem in Node.js — the create-only
attribute_not_existsput. - DynamoDB condition expressions — every function, with patterns.
- Enforcing uniqueness on multiple attributes — conditions + transactions combined.
- DynamoDB ConditionalCheckFailedException — when the failed check is expected, and how to handle it cheaply.
References
- UpdateItem — Amazon DynamoDB API Reference
- Condition expressions — Amazon DynamoDB Developer Guide
- DynamoDB read and write operations (capacity unit consumption) — Amazon DynamoDB Developer Guide
Last verified 2026-07-28 against the official AWS documentation linked above.