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

Created on 28 Dec 2018  路  11Comments  路  Source: aws/aws-sdk-js

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.

feature-request needs-major-version

Most helpful comment

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

All 11 comments

Thanks for submitting this feature request. I've tagged it so we can review for prioritization.

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()

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

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.

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

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! :)

If you have a specific fail code you are looking for, say a failed condition check in Put, then you can use RegExp on the err.message

const checkForConditionalCheckFailed = new RegExp(/ConditionalCheckFailed/gi);
checkForConditionalCheckFailed.test(err.message)

I see some solutions but they won't work in my case as we always use the .promise() and don't use callbacks for the calls to dynamodb. Is there any way to know which item failed in the transaction with the reason without parsing the message of the error ?
TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [None, None, ConditionalCheckFailed]

Used @paulswail solution. Works like a charm!

+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'!

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.

Was this page helpful?
0 / 5 - 0 ratings