TLDR: We've created our own Node.js template to automatically monitor AWS Lambda functions for errors. Get the code here.

AWS Lambda allows you to invoke a custom function in response to events such as an HTTP request, a message from SNS, an S3 event, or even to perform arbitrary units of work. The functions themselves -- called handlers -- can be written in Node.js, Java, or Python. Today we're going to take a look at how to set up a Node.js handler and monitor it for errors using Honeybadger for Node.js.

The anatomy of a handler

In Node.js, a handler is a simple JavaScript function with two arguments: event and context. The handler should be assigned to exports.handler inside the file that is loaded by AWS. Consider the following example:

exports.handler = function(event, context) {
  if (event.success === true) {
    context.succeed("Hello world");
  } else {
    context.fail("Goodbye, cruel world.");
  }
};

The event argument is the data passed into the function from the event source, which could be anything from a Kenesis stream to an API request. When it reaches your function, the event is just a simple JSON object, and therefore can be totally arbitrary. In this example, we could test this function with the following event data to make it succeed:

{
  "success": true
}

The context argument is provided by AWS Lambda and contains useful information about the runtime in addition to some important functions: context.succeed() and context.fail(). In order to properly terminate your Lambda function's execution, you must call one of these functions to indicate a successful or unsuccesful result. Otherwise, your function will continue to execute until the Node.js event queue is empty or the configured timeout is reached.

Creating your first Lambda Function

The following is an example handler provided by AWS. It takes an event including options and request data and makes an HTTPS request to the provided endpoint:

var https = require('https');

/**
 * Pass the data to send as `event.data`, and the request options as
 * `event.options`. For more information see the HTTPS module documentation
 * at https://nodejs.org/api/https.html.
 *
 * Will succeed with the response body.
 */
exports.handler = function(event, context) {
    var req = https.request(event.options, function(res) {
        var body = '';
        console.log('Status:', res.statusCode);
        console.log('Headers:', JSON.stringify(res.headers));
        res.setEncoding('utf8');
        res.on('data', function(chunk) {
            body += chunk;
        });
        res.on('end', function() {
            console.log('Successfully processed HTTPS response');
            // If we know it's JSON, parse it
            if (res.headers['content-type'] === 'application/json') {
                body = JSON.parse(body);
            }
            context.succeed(body);
        });
    });
    req.on('error', context.fail);
    req.write(JSON.stringify(event.data));
    req.end();
};

If you have an AWS account you can try creating a new Lambda function from this template here. To finish creating your function, give it a name and select a role ("Basic execution role" should work). The default memory and timeout settings are fine.

The heart of a Lambda function's execution is the event payload, which is just a JSON object. The event object can contain any data that the function needs to execute. If you test your new function using the default test event data from the AWS console, you'll probably see an error like this:

START RequestId: 53a5a8e9-d043-11e5-aa29-3fb208bb8a4a Version: $LATEST
2016-02-10T22:12:09.104Z  53a5a8e9-d043-11e5-aa29-3fb208bb8a4a  TypeError: Cannot read property 'protocol' of undefined
    at Object.exports.request (https.js<img class='emoji' title=':100:' alt=':100:' src='https://assets.github.com/images/icons/emoji/unicode/1f4af.png' height='20' width='20' align='absmiddle' />14)
    at exports.handler (/var/task/index.js:11:21)
END RequestId: 53a5a8e9-d043-11e5-aa29-3fb208bb8a4a
REPORT RequestId: 53a5a8e9-d043-11e5-aa29-3fb208bb8a4a  Duration: 192.04 ms Billed Duration: 200 ms   Memory Size: 128 MB Max Memory Used: 10 MB  
Process exited before completing request

Oops! It looks like we're missing some options that our handler needs to make the HTTP request, and the function is throwing an uncaught TypeError because the event.options key is missing. To make this function actually send an HTTP request we need to provide it with valid options and data (optional):

{
  "options": {
    "host": "encrypted.google.com",
    "port": 443,
    "method": "GET",
    "path": "/"
   }, "data": {
     "hello": "world"
   }
}

To configure the test event, select "Actions" -> "Configure test event" while viewing your Lambda function in the AWS console. Running the test again with the correct options should cause your test to succeed.

So that's great! We can use this function to securely send arbitrary data to any HTTPS endpoint we want, on-demand; a great use case for this is delivering WebHooks.

Monitoring Lambda Functions for errors

But what about that first error we encountered? And what about other errors? This function is fairly brittle; if you forget a required event key it will crash; if the HTTP response is invalid JSON (or sends the wrong Content-Type header) it will also crash.

The good news is that AWS Lambda provides built-in monitoring through Amazon CloudWatch. It's relatively easy to get pretty charts on anything from average invocation counts, durations, and even errors. CloudWatch also allows you to create alarms when a metric exceeds a threshold, so you could set up an alarm to send an SNS message when the error rate for your function exceeds a certain level.

If you want more fine-grained notifications about errors, however, you're on your own. Luckily, Honeybadger has you covered.

We've developed our own template for creating a Lambda function which reports all unhandled exceptions to Honeybadger. You can get the code here.

We're relying on some 3rd-party dependencies via NPM (including our own honeybadger client package for Node), so we're using the advanced scenario ("upload a zip file") method to deploy our handlers.

Reporting errors to Honeybadger

First, clone our template repo:

git clone git@github.com:honeybadger-io/honeybadger-lambda-node.git ./lambda_func
cd ./lambda_func

Next we're going to modify the index.js file to make HTTP requests using the Node.js HTTPS function template from before:

console.log("Loading function");


// Change to your Honeybadger.io API key.
const HB_API_KEY = 'your api key';


var https = require('https');

/**
 * Pass the data to send as `event.data`, and the request options as
 * `event.options`. For more information see the HTTPS module documentation
 * at https://nodejs.org/api/https.html.
 *
 * Will succeed with the response body.
 */
function handler(event, context) {
  var req = https.request(event.options, function(res) {
    var body = '';
    console.log('Status:', res.statusCode);
    console.log('Headers:', JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      body += chunk;
    });
    res.on('end', function() {
      console.log('Successfully processed HTTPS response');
      // If we know it's JSON, parse it
      if (res.headers['content-type'] === 'application/json') {
        body = JSON.parse(body);
      }
      context.succeed(body);
    });
  });
  req.on('error', context.fail);
  req.write(JSON.stringify(event.data));
  req.end();
}


// Takes a handler function and returns a new function which reports errors to
// Honeybadger.
function makeHandler(handler) {
  var Honeybadger = require("honeybadger"),
      Promise = require("promise");

  var hb = new Honeybadger({
    apiKey: HB_API_KEY,
    logger: console      // Required for events to be emitted.
  });

  var send = function(err, opts) {
    return new Promise(function(resolve, reject) {
      hb.once("error", reject).
         once("remoteError", reject).
         once("sent", resolve);
      hb.send(err, opts);
    });
  };

  return function(event, context) {
    try {
      handler.apply(this, arguments);
    } catch(err) {
      send(err, { context: { event: event } }).then(function() {
        context.fail(err);
      }).catch(function(sendErr) {
        console.error("Unable to report error to Honeybadger:", sendErr)
        context.fail(err);
      });
    }
  }
}

// Build and export the function.
exports.handler = makeHandler(handler);

Don't forget to change the HB_API_KEY constant to the API key for your project in Honeybadger. If you don't have an account you can create a 15-day free trial here.

Save the new index.js, and then build the .ZIP file:

npm install
make build

To replace the old lambda function with your monitored function, select "Upload a .ZIP file" as your code entry type instead of "Edit code inline". Follow the instructions to upload the .zip file and then test your function again (perhaps with an empty test event payload: {}).

You should now see the original error in Honeybadger, in realtime!

Further reading: