DynamoDB TransactWriteItems in Node.js (AWS SDK v3)
A successful TransactWriteItemsCommand tells you almost nothing: no items, no attributes, an empty response. Everything you need is on the exception, so in SDK v3 the catch block below is the real API surface, and it is worth knowing exactly what lands in it. (For when a transaction is the right call at all, see DynamoDB transactions.)
Code
import {DynamoDBClient, TransactWriteItemsCommand} from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({region: 'us-east-1'});
// Move one award between two songs — atomically. If the first song has no
// award to give, NEITHER update happens.
const command = new TransactWriteItemsCommand({
TransactItems: [
{
Update: {
TableName: 'Music',
Key: {Artist: {S: 'Arturo Sandoval'}, SongTitle: {S: 'Cubano Chant'}},
UpdateExpression: 'SET #upd0 = #upd0 - :one',
ConditionExpression: '#upd0 >= :one',
ExpressionAttributeNames: {'#upd0': 'Awards'},
ExpressionAttributeValues: {':one': {N: '1'}}
}
},
{
Update: {
TableName: 'Music',
Key: {Artist: {S: 'Arturo Sandoval'}, SongTitle: {S: 'A Mis Abuelos'}},
UpdateExpression: 'SET #upd0 = if_not_exists(#upd0, :zero) + :one',
ExpressionAttributeNames: {'#upd0': 'Awards'},
ExpressionAttributeValues: {':one': {N: '1'}, ':zero': {N: '0'}}
}
}
]
});
try {
await client.send(command);
console.log('Transaction committed');
} catch (err) {
if (err.name === 'TransactionCanceledException') {
// One reason per action, in TransactItems order. 'None' means that action
// was fine — some OTHER action sank the transaction.
const codes = (err.CancellationReasons ?? []).map((r) => r.Code);
console.log('Transaction canceled:', codes); // e.g. ['ConditionalCheckFailed', 'None']
} else {
throw err;
}
}Explanation
TransactItems— an ordered array ofPut,Update,DeleteandConditionCheckactions. Order is not execution order (the transaction is atomic), but it is the order the failure reasons come back in, which is the only reason to care about it. The caps are covered below.- What v3 actually throws — the caught object's own properties are
$fault,$retryable,$metadata,name,CancellationReasons,messageand__type. There is noerr.code;err.nameis the string to switch on, anderr.$metadatacarrieshttpStatusCode: 400plusattempts: 1, which is how you can tell the SDK did not quietly retry the cancellation for you. CancellationReasonsis positional and sparse. For the transaction above it arrives as[{"Code":"ConditionalCheckFailed","Message":"The conditional request failed"},{"Code":"None"}]. TheNoneentry has noMessageproperty at all, soerr.CancellationReasons.map((r) => r.Message.trim())throws inside your error handler on the very actions that succeeded.ReturnValuesOnConditionCheckFailure: 'ALL_OLD'adds anItemto that action's reason, ahead ofCodeandMessage, in raw DynamoDB JSON. The losing item's attributes come back for free; the alternative is a follow-upGetItemafter you already lost the race.- The
err.namecheck has a hole, and it is worth knowing which one. Point two actions at the same item and DynamoDB answersValidationExceptionwith the messageTransaction request cannot include multiple operations on one item, and noCancellationReasonsat all, because nothing was attempted. Theelse { throw err }branch above rethrows it. That is correct behavior, not a bug, but it means the structural mistakes never reach your cancellation logging. - v3 already sends a
ClientRequestToken, even when you omit it. Capturing the serialized body shows a fresh UUID on the wire, and twosend()calls of the same command object went out with two different tokens. So the token protects one call in flight, not your own retry loop: catch, resend, and you have a new token and no idempotency. Supply your own if a retry can cross a process boundary. Reuse it with any parameter changed and you getIdempotentParameterMismatchinstead of a silent double-apply. TransactionConflict— the one other code that needs a code path. It means a concurrent transaction held one of your items, so a retry with backoff is the right response whereConditionalCheckFailednever is. The rest are decoded on the TransactionCanceledException page.- Cost — every item in a transaction is written twice underneath (prepare, then commit), so budget roughly 2× the write capacity of a plain write. A single-item conditional write gives you atomicity on one item at half that.
Which limit you hit first
The 100-action cap and the 4 MB cap are independent, and the byte one is the one that surprises people: a hundred counter increments are nothing, while a dozen fat items can exhaust the aggregate on their own. Measure a representative item with the DynamoDB item size calculator before you decide how many actions to batch. To read the items an action will touch while you are still writing the condition, download DynoTable.
Related examples
- DynamoDB TransactWriteItems in Python — the same transaction with boto3.
- DynamoDB TransactWriteItems with the AWS CLI — the same transaction from the shell.
- DynamoDB conditional write in Node.js — single-item atomicity without the 2× cost.
- DynamoDB transactions — isolation, idempotency, and when transactions are worth it.
- DynamoDB TransactionCanceledException — every cancellation-reason code, decoded.
- "Too many actions in a TransactWriteItems call" — the 100-action and 4 MB transaction limits.
- "Transaction request cannot include multiple operations on one item" — one action per item, per transaction.
References
- TransactWriteItems — Amazon DynamoDB API Reference
- Amazon DynamoDB transactions: how it works — 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.