AppSync: how to error on DynamoDB conditional check failures

You can become a serverless blackbelt. Enrol to my 4-week online workshop Production-Ready Serverless and gain hands-on experience building something from scratch using serverless technologies. At the end of the workshop, you should have a broader view of the challenges you will face as your serverless architecture matures and expands. You should also have a firm grasp on when serverless is a good fit for your system as well as common pitfalls you need to avoid. Sign up now and get 15% discount with the code yanprs15!

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!

Liked this article? Support me on Patreon and get direct help from me via a private Slack channel or 1-2-1 mentoring.
Subscribe to my weekly newsletter


Hi, I’m Yan. I’m an AWS Serverless Hero and I help companies go faster for less by adopting serverless technologies successfully.

Are you struggling with serverless or need guidance on best practices? Do you want someone to review your architecture and help you avoid costly mistakes down the line? Whatever the case, I’m here to help.

Hire me.


Skill up your serverless game with this hands-on workshop.

My 4-week Production-Ready Serverless online workshop is back!

This course takes you through building a production-ready serverless web application from testing, deployment, security, all the way through to observability. The motivation for this course is to give you hands-on experience building something with serverless technologies while giving you a broader view of the challenges you will face as the architecture matures and expands.

We will start at the basics and give you a firm introduction to Lambda and all the relevant concepts and service features (including the latest announcements in 2020). And then gradually ramping up and cover a wide array of topics such as API security, testing strategies, CI/CD, secret management, and operational best practices for monitoring and troubleshooting.

If you enrol now you can also get 15% OFF with the promo code “yanprs15”.

Enrol now and SAVE 15%.


Check out my new podcast Real-World Serverless where I talk with engineers who are building amazing things with serverless technologies and discuss the real-world use cases and challenges they face. If you’re interested in what people are actually doing with serverless and what it’s really like to be working with serverless day-to-day, then this is the podcast for you.


Check out my new course, Learn you some Lambda best practice for great good! In this course, you will learn best practices for working with AWS Lambda in terms of performance, cost, security, scalability, resilience and observability. We will also cover latest features from re:Invent 2019 such as Provisioned Concurrency and Lambda Destinations. Enrol now and start learning!


Check out my video course, Complete Guide to AWS Step Functions. In this course, we’ll cover everything you need to know to use AWS Step Functions service effectively. There is something for everyone from beginners to more advanced users looking for design patterns and best practices. Enrol now and start learning!