Contents

Gracefully Handling Appsync Errors

The problem


  A well-written application should have well-written error handling. There are few things more frustrating to a developer than having code that eats exceptions with no trace of the error, instead only a null value returned from a callable. On the flip side exceptions can often dump out information that you users don’t need to know or are too cryptic to be useful. Proper error handling requires a understanding few things:

  • It is OK to raise Exceptions. There are times that it is better to let Python blow up rather than assume that catching then returning will stop the rest of our code
  • We should ALWAYS know what the user will get back in the response. Clients should be on a “need to know” basis. They do NOT need to know AWS arn’s, account numbers, resource names, etc. All of these can easily be leaked out by boto3 exceptions.
  • Users should get back a message that is useful. At the very least the user should know whether the error is due to something they did or something in the backend that is out of their control

This post focuses on how to handle these errors in a graceful way. Since I tend to use Lambda as the backend datasource for Appsync I will show a trick that I use that combines a Direct Lambda datasource and a VTL response template to keep our error response to the front end clean while still keeping the info we need for debugging code.

The Solution


  When I create an Appsync application I will normally create a base exception class with more specific exceptions inheriting from it. Here is an example structure:

exceptions.py

class MyAppError(Exception):
    def __init__(self, *args, **kwargs):
        default_message = "MyApp generic error"
        if not (args or kwargs): args = (default_message,)
        super().__init__(*args, **kwargs)

class YouGoofedError(MyAppError):
    def __init__(self, *args, **kwargs):
        default_message = "MyApp error that I show to clients"
        if not (args or kwargs): args = (default_message,)
        super().__init__(*args, **kwargs)

Now we update our Lambda handler to discern between the exceptions we want to pass through to the response template and the ones we don’t

function.py

from exceptions import (
    MyAppError,
    YouGoofedError
)


def handler(event, _):
    try:
        handle_route()
    except Exception as e:
        if isinstance(e, MyAppError):
            # For any exception that is part of our internal exception class
            # we want to flag it for handling in our response template
            res = {
                "myapp_error": str(e),
                "error_type": e.__class__.__name__,
            }
        else:
            raise e

    return res


def handle_route():
    raise YouGoofedError("You passed a value in your mutation that I didn't like")

As you can see, when this lambda is called we are going to immediate raise our custom exception. Now we create a response template that will discern whether the error was intentionally raised or not.

response_template.vtl

#if ($!ctx.result.myapp_error)
  ## Custom errors from our error class then will return the error definition from
  ## the try/except clause of our Lambda
  $util.error($ctx.result.myapp_error, $ctx.result.error_type)
#elseif ($!ctx.error)
  ## This will set the error type in the Appsync response to "InternalError",
  ## similar to an http 500 error, if this is an unhandled exception on our part.
  $util.error("MyApp Internal Error. It's not you, it's us...", "InternalError")
#else
  ## Looks like we made it. Return the result
  $util.toJson(${ctx.result})
#end

Now when we raise an exception due to some unforeseen problem in our code we will return a sanitized error response to the frontend, similar to what you would get from an http 500 response, while still raising the exception internally so that our script behaves the way we would expect when an exception happens. We have also created the ability to raise custom exceptions that will halt the execution of our Lambda and let the user know they did something that our app didn’t like, for instance, a validation of user input.

Wrapping up


  Proper handling of exceptions allows the developer to maintain the ability to debug code, halt execution before more damage is done, and provide the user with a controlled error response without having to write a million blocks of code that rewrite error messages, return early on a caught exception and risk code executing after it should have stopped, or run the risk of exposing sensitive data or information that could be used for recon against your application.

About the Author

Linux loving , Python slinging, OpenSource evangelizing Senior Solutions Architect at Quinovas