DynamoDB UpdateItem in Node.js (AWS SDK v3)

The low-level v3 client speaks DynamoDB JSON in both directions, which means every number you send and every number you get back is a string. That is not a wart; it is the only way a 38-digit DynamoDB number survives a language whose only number type is a double. It is also where the bugs are.

Code

import {DynamoDBClient, UpdateItemCommand} from '@aws-sdk/client-dynamodb';

const client = new DynamoDBClient({region: 'us-east-1'});

const command = new UpdateItemCommand({
  TableName: 'Music',
  Key: {
    Artist: {S: 'Arturo Sandoval'},
    SongTitle: {S: 'Cubano Chant'}
  },
  UpdateExpression: 'SET #upd0 = :updValue0, #upd1 = :updValue1 ADD #upd2 :updValue2',
  ExpressionAttributeNames: {
    '#upd0': 'Genre',
    '#upd1': 'Year',
    '#upd2': 'Awards'
  },
  ExpressionAttributeValues: {
    ':updValue0': {S: 'Latin Jazz'},
    ':updValue1': {N: '1994'},
    ':updValue2': {N: '1'}
  },
  ReturnValues: 'ALL_NEW'
});

const response = await client.send(command);
console.log(response.Attributes); // the item after the update

Against an item that had no Genre and no Awards, response.Attributes comes back as:

{"Artist":{"S":"Arturo Sandoval"},"Awards":{"N":"1"},"Genre":{"S":"Latin Jazz"},"Year":{"N":"1994"},"SongTitle":{"S":"Cubano Chant"}}

typeof response.Attributes.Awards.N is "string", so response.Attributes.Awards.N + 1 evaluates to "11". Nothing throws, nothing warns, and the wrong number goes into your next write. Parse at the boundary: Number(response.Attributes.Awards.N).

Explanation

  • The expression is a plain string, and v3 will not check it. UpdateItemCommand validates the shape of the input object, never the grammar inside UpdateExpression, so a typo is a round trip and a 400. The grammar is in update expressions; ADD #upd2 :updValue2 is the atomic increment, and adding ConditionExpression: 'attribute_exists(Artist)' makes the call update-only rather than an upsert.

  • ReturnValues: 'UPDATED_NEW' is usually the one you want. The same update returns {"Awards":{"N":"2"}} and nothing else. ALL_NEW ships the whole item back on every call, which on a fat item is bandwidth you are paying for to read one counter.

  • $metadata is v3's out-of-band channel: {"httpStatusCode":200,"requestId":"...","attempts":1,"totalRetryDelay":0}. attempts is the honest answer to "did this retry", which matters when you are reasoning about whether a non-idempotent write ran twice.

  • ValidationException is not a class you can catch, only a name you can compare. A missing alias comes back as err.name === 'ValidationException' with err.message set to Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: Year.

  • The document client is the other trade. @aws-sdk/lib-dynamodb takes native JS values and unmarshalls the response, at the cost of that string safety. marshall({awards: 9007199254740993}) from @aws-sdk/util-dynamodb refuses outright:

    Number 9007199254740992 is greater than Number.MAX_SAFE_INTEGER. Use NumberValue from @aws-sdk/lib-dynamodb.

    Look closely at the number in that message. It ends in 2, not the 3 that was written in the literal: JavaScript had already rounded it before the SDK ever saw it. The low-level client in this snippet cannot have that problem, because {N: '9007199254740993'} is text all the way to the wire.

What a failed condition hands you

Add ReturnValuesOnConditionCheckFailure: 'ALL_OLD' to the input and the thrown error carries the item that beat you:

name: ConditionalCheckFailedException | message: "The conditional request failed" | http: 400
err.Item: {"Artist":{"S":"Arturo Sandoval"},"Awards":{"N":"2"},"Year":{"N":"1994"},"SongTitle":{"S":"Cubano Chant"},"Genre":{"S":"Latin Jazz"}}

err.Item is raw DynamoDB JSON regardless of which client threw it, and it is free. Without it, the honest way to find out why an optimistic-concurrency update failed is a follow-up GetItem that costs a read and may already be stale again.

The DynamoDB JSON converter turns that payload into a plain JS object and back, which is the fastest way to build a fixture from a real item. To pull that item off a live table in the first place, 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.