Every unhandled exception is a bug in your software. Things will go wrong in your Lambda function, and you want to know about it.
Building a wrapper
Raven, a library maintained by Sentry, abstracts away all the hard parts of sending errors to Sentry. What we need to take care of is to:
- Catch all unhandled exceptions,
- Pass them to Sentry using
- Return the appropriate HTTP response to the client, and
- Ensure that the Lambda handler terminates correctly.
The natural way to achieve handling of all exceptions in a Lambda handler is to wrap it in a
try..catch block. Server-less APIs usually consist of several endpoints; we want to apply error handling to all Lambda handlers without repeating to much boilerplate code. Ideally, we would implement error handling in a wrapper module, that we can apply to each Lambda handler in our application.
Instead of directly exporting the handler function, we export the return value of
SentryWrapper.handler, which returns a function that executes
lambdaHandler while also catching all errors raised during execution.
How this works becomes more clear when we look at
sentryHandler is a higher-order function, that takes the
lambdaHandler as an argument and returns a function that executes
lambdaHandler. Since we’re exporting the returned result in
lambdaHandler.js, the returned function is as the Lambda handler, which in turn executes the original
lambdaHandler. The only difference is the
try..catch block around it.
Now that we have set up basic error handling, we can take care of reporting the error to Sentry. The library
Raven, maintained by Sentry, provides the functionality we need to report errors to Sentry. So it’s a matter of setting up the
Raven client and reporting the error using
Raven.captureException. In the example below, we’re reading the Sentry DSN from the environment variable
Raven.captureException is an asynchronous function and the Lambda handler doesn’t wait for its successful execution. Since
Raven.captureException doesn’t return a value, it considers all operations completed and completes the execution of the Lambda handler itself, which also terminates all ongoing asynchronous operations. The error is never logged with Sentry.
captureException, however, accepts a function as a second parameter, which acts as the callback after the error is logged with Sentry. We can use that to ensure the error reported before terminating the Lambda handler. By returning a
Promise after we caught an exception we can force the Lambda execution to stay active until the
Promise is resolved or rejected. We can resolve the
Promise inside the
captureException callback; i.e., as soon as the exception was successfully logged with Sentry. Resolving the
Promise with an HTTP 500 error response also allows us to notify the client about the error. We extend our previous example accordingly:
If you use TypeScript or ES2016 to write your application, you’re going to deploy compiled source to AWS. The stack trace on Sentry then shows the compiled, unreadable code, which difficult to debug. Source maps provide the mapping from the compiled code to the original source, thus allowing you to debug the original, usually more readable source. Whatever processing tool you use, there is usually a way to enable the creation of source maps next to the compiled sources. With Webpack it’s as simple as enabling
devtool: 'source-map' in the Webpack config.
To use source maps on Sentry, we need to upload the source maps as artefacts to Sentry. Artefacts exist in the context of a release; for each release, you use a different set of source maps. It’s important to specify the release both for when you upload source maps as artefacts and when you log an error. I won’t go into the details of uploading source maps to Sentry as they have already documented various ways of doing that. Again, we have used Webpack to build our code and the plugin
SentryPlugin to upload artefacts during the build process.
Since the source maps are attached to a release in Sentry, we need to identify the release deployed to out Lambda function. The current release is defined when setting up the
The final thing that is left to do is to normalise the paths of the script that we send to Sentry so the source maps can be mapped accordingly. The naming of source maps uploaded to Sentry follows the path in the output directory of the built scripts. If the file that contains the error is built to
/app_path/dist/projects/create.js then Sentry expects to find the source map under
~/projects/create.js.map, which is how the artefact is named in Sentry.
On AWS Lambda, however, the script is usually hosted under
/var/task/, resulting in the path
/var/task/projects/create.js. In the event of an exception, this is the path that appears on the corresponding stack trace, so Sentry won’t be able to resolve the built code to its original source. It can’t find a source map called
Fortunately, we can rewrite the path using in a
dataCallback function, which is defined in the Raven config and allows us to rewrite the data that is sent to Sentry. Sentry’s documentation on TypeScript already provides us with a good example of how to do the rewrite. When we run our app on AWS, we can assume that the
root directory resolves to
/var/task, so we can use a constant here instead of working out the directory dynamically.
After adding the data callback, the final result should look like this: