Escrita condicional no DynamoDB em Node.js (AWS SDK v3)

A parte interessante de uma escrita condicional no AWS SDK v3 não é a ConditionExpression, que funciona igual em todo lugar e está coberta em expressões de condição do DynamoDB. É o caminho de falha: a v3 te entrega o item perdedor no erro lançado, se você o pediu, e não te dá nada se não pediu.

Código

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

Explicação

  • A checagem que falha é um erro lançado, não um campo de status. A v3 rejeita a promise, então o caminho da escrita e o caminho da corrida perdida são ramos diferentes. err.name === 'ConditionalCheckFailedException' é o discriminador; qualquer outra coisa precisa ser relançada, que é para isso que serve o else no bloco. Engula o catch inteiro e você terá transformado silenciosamente um throttle em um no-op.
  • ReturnValuesOnConditionCheckFailure é a única forma de ver quem te venceu. Sem ele, o erro carrega a mensagem e nada mais, e você volta para um GetItem que não precisava. A referência da API define seus valores válidos como ALL_OLD | NONE e confirma que ele não consome capacidade de leitura.
  • err.Item é um mapa AttributeValue bruto, com o mesmo formato da Key que você enviou, e não JavaScript comum. Passe-o pelo unmarshall do @aws-sdk/util-dynamodb antes de comparar Version com um número, ou você estará comparando com {N: '9'}.
  • A escrita que falhou ainda é cobrada. O Developer Guide é explícito ao dizer que uma condição que avalia como falsa ainda consome capacidade de escrita, dimensionada pelo maior entre o item antigo e o novo. Um laço de retry em uma chave quente é uma linha real na conta, então limite as tentativas.
  • Todo nome no bloco está com alias (#versionVersion, #cond0Artist) porque o Expression Builder que o gerou coloca alias incondicionalmente. Isso é mais pesado que o necessário aqui e nunca está errado, que é a troca que ele faz.

Lendo a cópia do perdedor a partir da exceção

Coloque o Version armazenado em 9 e rode o bloco, que espera 7. O DynamoDB Local 3.3.0 lança o erro, e o erro capturado carrega:

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

Esse Version: 9 é o ponto todo. O retry pode voltar direto ao update com :expectedVersion em 9, sem leitura extra e sem uma janela na qual um terceiro escritor se meta entre o seu GetItem e o seu retry.

Apague ReturnValuesOnConditionCheckFailure do mesmo comando e rode de novo. Mesmo name, mesma message, mesmo 400, e err.Item é undefined. Nada te avisa: o parâmetro é opcional, a ausência dele não é um erro, e o código que lê err.Item simplesmente começa a registrar undefined em produção.

Repare também que um 400 aqui não significa uma requisição malformada. ValidationException e ConditionalCheckFailedException compartilham o código de status, e só um deles é um bug — por isso o ramo é feito em err.name e nunca no status.

Para ver uma condição ter sucesso e falhar contra os seus próprios dados, com a expressão escrita para você em vez de digitada, baixe o DynoTable.

Exemplos relacionados

Referências

Verificado pela última vez em 2026-07-28 contra a documentação oficial da AWS vinculada acima.

Trabalhe com o DynamoDB sem o Console

Um cliente desktop rápido para DynamoDB que roda o SQL de verdade que o DynamoDB não consegue — JOINs, GROUP BY, agregações — com edição visual e um agente de IA com suas próprias chaves do Bedrock.

Teste grátis de 30 dias, sem cartão de crédito — depois o plano Grátis sem limite de tempo.