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.
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 theSport
table usingPutItem
updateSport
: updates an existing row in theSport
table usingUpdateItem
to update just thedisplayName
andimageUrl
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:
- 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.
- 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.
- Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.