AppSync: how to error on DynamoDB conditional check failures

Yan Cui

I help clients go faster for less using serverless technologies.

This article is brought to you by

Don’t reinvent the patterns. Catalyst gives you consistent APIs for messaging, data, and workflow with key microservice patterns like circuit-breakers and retries for free.

Try the Catalyst beta

TL;DR

To make an AppSync DynamoDB resolver throw exceptions on conditional check errors, we need to check $context.error in the response mapping template ourselves. Like this:

#if ( $ctx.error )
  #if ( $ctx.error.type.equals("DynamoDB:ConditionalCheckFailedException") )
    $util.error("your error message")
  #else
    $util.error($ctx.error.message, $ctx.error.type)
  #end
#end
$utils.toJson($context.result)

And now, the longer version.

The problem

AppSync lets us perform DynamoDB operations (GetItem, PutItem, UpdateItem etc.) without having to write any custom Lambda functions.

For example, to perform PutItem against a DynamoDB table, we need a request template like this:

{
  "version" : "2018-05-29",
  "operation" : "PutItem",
  "key": {
    "name" : $utils.dynamodb.toDynamoDBJson($context.arguments.name)
  },
  "attributeValues" : {
    "imageUrl" : $utils.dynamodb.toDynamoDBJson(...),
    "displayName" : $utils.dynamodb.toDynamoDBJson(...)
  }
}

Often, we want to have separate add and update mutations rather than just a single put mutation. For instance, in a social network app I’m building for a client we have a pre-configured list of sports users can do together.

We want to track the no. of users who declare their interest in doing a sport with others. So we added a count attribute to each sport.

Every time a user updates his/her sports preference, we’ll process the change through the DynamoDB Stream from the User table. The OnProfileUpdate function calculates the deltas and updates the count attribute in the aforementioned Sport table.

Through our CMS, admins can add new sports or change the displayName and/or imageUrl to show for each sport. But we don’t want to override the count attribute, which is updated by this OnProfileUpdate function.

So we need two mutations:

  • addSport : adds a new row to the Sport table using PutItem
  • updateSport : updates an existing row in the Sport table using UpdateItem to update just the displayName and imageUrl attribtues.

In both cases, we need to use condition expressions to ensure the sport doesn’t exist (for addSport) or it exists already (for updateSport). And we want to throw an appropriate error if the conditional check fails.

And that’s where the problem is – even when the conditional checks fail, the DynamoDB resolver did not return an error.

Given the following template for the addSport mutation:

The following addSport request should have resulted in an error, at least according to the documented behaviour. The displayName we tried to write was different from the existing data.

Similarly, an updateSport mutation with a non-existent sport should have thrown an error. But instead, it simply returned null.

What’s going on here?

Since the official examples are still referencing template version 2017–02–28 (which is no longer supported), I suspect the behaviour has changed in template version 2018–05–29.

For both PutItem and UpdateItem, when there is a conditional check error the resolver appears to fetch the current item and save it in the $context.result. This happens regardless if the item is different.

This is neither the behaviour I intuitively expect to see nor the behaviour that is current documented. I have raised this documentation issue through the Feedback channel on the documentation page. Hopefully, it’ll be addressed in the near future. In the meantime, if you run into this problem as I did then I hope this post helps you in some way.

The solution

When the DynamoDB resolver catches a ConditionalCheckFailedException, it also stores the error in $context.error.

In the response mapping template, you can inspect this attribute yourself and either rethrow the error or throw a different error altogether.

With this simple change, I was able to get the desired behaviour for both addSport and updateSport mutations.

This solution is based on the example from the official changelog, but there’s a typo in the example there. I have reported this typo so hopefully, it’ll be fixed soon. I have verified the snippet I shared above, so use that for now.

I have had a lot of fun working with AppSync this past few weeks and I will share more of my experience with you soon. Watch this space.

Until next time!

Whenever you’re ready, here are 3 ways I can help you:

  1. Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
  2. I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
  3. Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.