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 of Put, Update, Delete and ConditionCheck actions. 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, message and __type. There is no err.code; err.name is the string to switch on, and err.$metadata carries httpStatusCode: 400 plus attempts: 1, which is how you can tell the SDK did not quietly retry the cancellation for you.
  • CancellationReasons is positional and sparse. For the transaction above it arrives as [{"Code":"ConditionalCheckFailed","Message":"The conditional request failed"},{"Code":"None"}]. The None entry has no Message property at all, so err.CancellationReasons.map((r) => r.Message.trim()) throws inside your error handler on the very actions that succeeded.
  • ReturnValuesOnConditionCheckFailure: 'ALL_OLD' adds an Item to that action's reason, ahead of Code and Message, in raw DynamoDB JSON. The losing item's attributes come back for free; the alternative is a follow-up GetItem after you already lost the race.
  • The err.name check has a hole, and it is worth knowing which one. Point two actions at the same item and DynamoDB answers ValidationException with the message Transaction request cannot include multiple operations on one item, and no CancellationReasons at all, because nothing was attempted. The else { throw err } branch above rethrows it. That is correct behaviour, 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 serialised body shows a fresh UUID on the wire, and two send() 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 get IdempotentParameterMismatch instead 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 where ConditionalCheckFailed never 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.

References

Last verified 2026-07-28 against the official AWS documentation linked above.

Work with DynamoDB without the Console

A fast DynamoDB desktop client that runs the real SQL DynamoDB can’t — JOINs, GROUP BY, aggregates — with visual editing and an AI agent on your own Bedrock keys.

Free 30-day trial, no credit card — then the Free plan with no time limit.