DynamoDB TransactWriteItems en Node.js (AWS SDK v3)

Un TransactWriteItemsCommand con éxito no te dice casi nada: sin Items, sin atributos, una respuesta vacía. Todo lo que necesitas está en la excepción, así que en el SDK v3 el bloque catch de abajo es la superficie real de la API, y vale la pena saber exactamente qué aterriza en él. (Para saber cuándo una transacción es la llamada correcta, mira transacciones en DynamoDB.)

Código

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;
  }
}

Explicación

  • TransactItems — un array ordenado de acciones Put, Update, Delete y ConditionCheck. El orden no es el orden de ejecución (la transacción es atómica), pero es el orden en que vuelven los motivos del fallo, que es la única razón para que te importe. Los límites se cubren más abajo.
  • Qué lanza realmente v3. Las propiedades propias del objeto capturado son $fault, $retryable, $metadata, name, CancellationReasons, message y __type. No hay err.code; err.name es la cadena sobre la que ramificar, y err.$metadata lleva httpStatusCode: 400 más attempts: 1, que es como puedes saber que el SDK no reintentó la cancelación por su cuenta.
  • CancellationReasons es posicional y disperso. Para la transacción de arriba llega como [{"Code":"ConditionalCheckFailed","Message":"The conditional request failed"},{"Code":"None"}]. La entrada None no tiene ninguna propiedad Message, así que err.CancellationReasons.map((r) => r.Message.trim()) lanza un error dentro de tu manejador de errores precisamente por las acciones que sí funcionaron.
  • ReturnValuesOnConditionCheckFailure: 'ALL_OLD' añade un Item al motivo de esa acción, por delante de Code y Message, en DynamoDB JSON en crudo. Los atributos del Item que hizo fallar la condición vuelven gratis; la alternativa es un GetItem posterior cuando ya has perdido la carrera.
  • La comprobación de err.name tiene un agujero, y vale la pena saber cuál. Apunta dos acciones al mismo Item y DynamoDB responde ValidationException con el mensaje Transaction request cannot include multiple operations on one item, y sin ningún CancellationReasons, porque no se intentó nada. La rama else { throw err } de arriba lo relanza. Ese es el comportamiento correcto, no un bug, pero significa que los errores estructurales nunca llegan a tu registro de cancelaciones.
  • v3 ya envía un ClientRequestToken, incluso cuando lo omites. Capturar el cuerpo serializado muestra un UUID nuevo en el cable, y dos llamadas a send() del mismo objeto de comando salieron con dos tokens distintos. Así que el token protege una llamada en vuelo, no tu propio bucle de reintento: capturas, reenvías, y tienes un token nuevo y ninguna idempotencia. Aporta el tuyo si un reintento puede cruzar la frontera de un proceso. Reutilízalo con cualquier parámetro cambiado y obtienes IdempotentParameterMismatch en vez de una doble aplicación silenciosa.
  • Solo otro código necesita su propio camino. TransactionConflict significa que una transacción concurrente retenía uno de tus Items, así que un reintento con backoff es la respuesta correcta ahí donde nunca lo es para ConditionalCheckFailed. El resto están descifrados en la página de TransactionCanceledException.
  • Coste — cada Item de una transacción se escribe dos veces por debajo (preparar y luego confirmar), así que presupuesta más o menos 2× la capacidad de escritura de una escritura normal. Una escritura condicional de un solo Item te da atomicidad sobre ese Item por la mitad.

Con qué límite chocas primero

El tope de 100 acciones y el de 4 MB son independientes, y el de bytes es el que sorprende: cien incrementos de contador no son nada, mientras que una docena de Items gordos pueden agotar el agregado ellos solos. Mide un Item representativo con la calculadora de tamaño de Item de DynamoDB antes de decidir cuántas acciones agrupar. Para leer los Items que una acción va a tocar mientras aún estás escribiendo la condición, descarga DynoTable.

Ejemplos relacionados

Referencias

Verificado por última vez el 2026-07-28 contra la documentación oficial de AWS enlazada arriba.

Trabaja con DynamoDB sin la Consola

Un cliente de escritorio rápido para DynamoDB que ejecuta el SQL real que DynamoDB no puede — JOINs, GROUP BY, agregaciones — con edición visual y un agente de IA con tus propias claves de Bedrock.

Prueba gratuita de 30 días, sin tarjeta — después, el plan Free sin límite de tiempo.