aws-sdk-js: No way to get reasons from TransactionCanceledException error

When attempting to use ConditionExpression and ReturnValuesOnConditionCheckFailure in a DynamoDB TransactWrite request, if the condition expression fails, there’s no way to get the return values from the error that is returned:

{
  message: 'Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]',
  code: 'TransactionCanceledException',
  time: 2018-12-28T19:23:43.251Z,
  requestId: '1QBFJMKSHKHD1OPCSCJGPUTBORVV4KQNSO5AEMVJF66Q9ASUAAJG',
  statusCode: 400,
  retryable: false,
  retryDelay: 45.44347093786354
}

This is particularly frustrating when there are multiple conditional checks that can fail and they have significantly different meanings.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 70
  • Comments: 15 (3 by maintainers)

Most upvoted comments

Any suggestions or links to example code would be extremely welcome.

Hey @claranet-barney , I hit this issue and created the following helper function to wrap my calls to transactWrite. Note that I’m using the DocumentClient rather than the raw DynamoDB client, but I think you could try the same approach with it:

function executeTransactWrite(params) {
    const transactionRequest = docClient.transactWrite(params);
    let cancellationReasons;
    transactionRequest.on('extractError', (response) => {
        try {
            cancellationReasons = JSON.parse(response.httpResponse.body.toString()).CancellationReasons;
        } catch (err) {
            // suppress this just in case some types of errors aren't JSON parseable
            console.error('Error extracting cancellation error', err);
        }
    });
    return new Promise((resolve, reject) => {
        transactionRequest.send((err, response) => {
            if (err) {
                console.error('Error performing transactWrite', { cancellationReasons , err });
                return reject(err);
            }
            return resolve(response);
        });
    });
}

and Typescript version:

function executeTransactWrite(
    params: DocumentClient.TransactWriteItemsInput,
): Promise<DocumentClient.TransactWriteItemsOutput> {
    const transactionRequest = docClient.transactWrite(params);
    let cancellationReasons: any[];
    transactionRequest.on('extractError', (response) => {
        try {
            cancellationReasons = JSON.parse(response.httpResponse.body.toString()).CancellationReasons;
        } catch (err) {
            // suppress this just in case some types of errors aren't JSON parseable
            console.error('Error extracting cancellation error', err);
        }
    });
    return new Promise((resolve, reject) => {
        transactionRequest.send((err, response) => {
            if (err) {
                console.error('Error performing transactWrite', { cancellationReasons , err });
                return reject(err);
            }
            return resolve(response);
        });
    });
}

An example of the cancellationReasons returned is:

[ { Code: 'ConditionalCheckFailed',
    Message: 'The conditional request failed' },
  { Code: 'ConditionalCheckFailed',
    Message: 'The conditional request failed' },
  { Code: 'ConditionalCheckFailed',
    Message: 'The conditional request failed' },
  { Code: 'ConditionalCheckFailed',
    Message: 'The conditional request failed' } ]

FYI it seems that it is fixed in AWS SDK v3

// TransactionCanceledException
{
  ...
  CancellationReasons: [
    {
      Code: 'ConditionalCheckFailed',
      Item: undefined,
      Message: 'The conditional request failed'
    },
    { Code: 'None', Item: undefined, Message: undefined }
  ]
}

For anyone that needs to access the CancellationReasons right now, they could be parsed out of the response body directly:

var request = ddb.transactWriteItems(params)

request.on('extractError', (resp) => {
  console.log(resp.httpResponse.body.toString());
});

request.send()

You can also just extend the existing error with another attribute:

  const request = await documentClient.transactWrite(params);

  request.on('extractError', (response) => {
    if (response.error) {
      const cancellationReasons = JSON.parse(response.httpResponse.body.toString()).CancellationReasons;
      response.error.cancellationReasons = cancellationReasons;
    }
  });

  const res = await request.promise();

You’ll probably get into some trouble if you want to make a clean with TS as you can’t change the error type 😕 Therefor you are probably stuck with @paulswail solution.

Any update on this? Even adding the array of reasons to the error object is better than only having it in the message.

Thanks @paulswail - thats pretty similar to the solution I ended up with and it works pretty well. It feels a little bit janky having to wrap it like this, so it would be great if the DynamoDB SDK team could get it wrapped in as part of the resolve response (as suggested by @jacktuck) although i guess there may be good reasons why thats not as easy as it sounds! 😃

Any update on this? I’ve hit the same issue and am finding that without being able to access the Cancellation reason, transactions are essentially unusable for the majority of places a classic transaction would be used.

The comment from @srchase on 28th Dec 2018 looks interesting, but i’m not familiar with this syntax and can’t seem to find any examples on how this is working.

The aws-sdk version I am using is 2.472.0, and the API version is 2012-08-10

Any suggestions or links to example code would be extremely welcome.

It would be sweet if this made it into the documentation. Even in the new docs it still claims

If using Java, DynamoDB lists the cancellation reasons on the CancellationReasons property. This property is not set for other languages.

If anyone has this problem with Python:

except client.exceptions.TransactionCanceledException as e:
  print(e.response)

+1 for @paulswail turn your .promise() into a manual resolve/reject promise and extract the error. Just took me a couple of hours to find out it was a spelling mistake in ‘partitionKey’!

Used @paulswail solution. Works like a charm!