CloudWatch Monitoring with Try/Catch Blocks in Your Node Lambdas

Luke Miller
6 min readJun 26, 2020

If you’re using a Node.js environment for your Lambda and Try/Catch blocks to encapsulate your code then CloudWatch won’t be able to gather errors for monitoring metrics since the Catch block allows you to gracefully handle errors. This means no helpful alarms to inform you of Lambda invocations that errored.

But, I’m going to show you a work around.

Let’s take a look at some examples of Lambdas to illustrate the problem.

Lambda Without Try/Catch Block

exports.handler = async (event, context) => {
const result = nonExistentFunction();

if(result != null){
return {
statusCode: 201,
body: JSON.stringify(result)
}
}
else{
return {
statusCode: 500,
body: “An error occurred.”
}
}
}

Explanation: Since nonExistentFunction is not defined anywhere, when Node tries to run nonExistentFunction it will throw a Reference Error. The if/else statement wont run in that case, and the Lambda will end without a return value. This constitutes an error, since our code did not complete and nothing was returned, and CloudWatch will record this error in the metrics and a corresponding log. If we had alarms set up with those metrics then we’d get a friendly notice of our failing code so that we can fix it.

Although this code sample allows us to benefit from CloudWatches monitoring, we probably don’t want our code to be allowed to fail like this without returning more useful information about the nature of the failure. If this was a part of an API for our front-end authentication process for instance, we’d want to provide the user with more specific feedback. A Try/Catch block can help with that.

Try/Catch Block -

exports.handler = async (event, context) => {
try{
const result = nonExistentFunction();
return {
statusCode: 201,
body: JSON.stringify(result)
}
}
catch(error){
return {
statusCode: 500,
body: "An error occurred."
}
}
}

Explanation: Now, when nonExistentFunction is attempted, the error occurs, and execution of the code is transferred to the Catch block. The code then returns a status code of 500 and our Lambda concludes. This illustrates why Try/Catch blocks are handy. It allows our code to error, but fails gracefully so that the program itself doesn’t terminate and we can provide a more informative response.

But, now our code isn’t failing as CloudWatch expects. It errors, yes, but we handle it with the Catch so the Lambda as a whole does not error. This means CloudWatch will not recognize this as an error for its metrics, and we cannot receive the monitoring alarms that we want.

If we left it like this, thousands of invocations of this Lambda could occur without us being aware that our code is repeatedly tripping the Catch block. Let’s solve this.

There are plenty of third-party Lambda monitoring services you can reach out to for assistance with monitoring. Depending on your needs though, that may not be in the budget or necessary for your project’s current stage. For you DIYers out there, here is how you can still leverage CloudWatch monitoring with Try/Catch blocks.

Adding CloudWatch Monitoring to Try/Catch Block

What we want to accomplish:
1. Record a metric of an error occurring in CloudWatch
2. Set up an alarm for a threshold of the metrics

First, make sure you have the aws-sdk and CloudWatch methods defined in your Lambda.

const AWS = require(‘aws-sdk’)
const cloudwatch = new AWS.CloudWatch({apiVersion: “2010–08–01”});

Great, now we have the aws-sdk and CloudWatch defined. Here is the full documentation for the AWS JavaScript SDK, but we’ll only be using the PutMetricData and PutMetricAlarm methods.

You’ll need three values in the parameter objects. I assigned them to these three variables that you’ll see referenced in the code below:

yourMetricName = the CloudWatch metric that will track occurrences of this Lambda erroring, and I use the name of my Lambda

yourMetricNamespace = in the CloudWatch metrics dashboard this is the parent container for your metrics, and I use the name of the application associated to the Lambda

yourAlarmName = I use the name of my Lambda + “ Errors”

Optionally, you can pass another value which I assigned to the actionARN variable, which is the action you want to occur if this alarm is triggered. Here’s the documentation, but some examples of what this could be is an ARN for stopping or rebooting an EC2, an SNS topic, or passing a scaling policy to an auto scaling group. If no actionARN is set, then when this alarm is triggered the only thing that will happen is the presence of a red identifier in your CloudWatch dashboard.

The Code:

const sendToCloudWatch(context){
const metricParams = {
MetricData: [
{
MetricName: <yourMetricName>,
Unit: “Count”,
Value: 1.0,
Timestamp: new Date()
}
],
Namespace: <yourMetricNamespace>
};
await cloudwatch.putMetricData(metricParams).promise(); const alarmParams = {
AlarmName: <yourAlarmName>
ComparisonOperator: “GreaterThanOrEqualToThreshold”,
EvaluationPeriods: 5,
AlarmActions: [<actionARN>],
MetricName: <yourMetricName>,
Period: 360,
Namespace: <yourMetricNamespace>,
Statistic: “Sum”,
Threshold: 1
};
await cloudwatch.putMetricAlarm(alarmParams).promise(); return;};

Here are AWS’s API documentation pages for PutMetricData and PutMetricAlarm.

A quick break down.

  1. We are passing to PutMetricData an object that says “I have one count of an error at this time and it should be filed under this metric namespace.” If the metric has never been made, this will create it, otherwise this just adds one more tick to the tally of errors that have occurred.
  2. We are passing to PutMetricAlarm an object that creates or updates an alarm in CloudWatch based off of the metric we just created/updated. I set the Threshold to 1, and the Statistic to “Sum”, and Period to 360 (seconds). This means that if the metric count at least 1 for the metric then this alarm will trigger. The triggering of the alarm sets off the AlarmAction, which is an arn of the SNS topic I previously created, and for me that’s sends me an email informing me of the Lambda that’s erroring.

Implementation

Either stick this in your Catch block, create a module from it to import throughout the Lambdas of your application, or (my preferred approach) make this a Lambda Layer and use it across all AWS applications (more about Layer’s here). If you choose to make this a module or Layer you’ll need to make the value you create the metric name and alarm dynamic, because in the sample above we just hardcoded our Lambda’s name. To do this, pass the Lambda’s Context object which contains the name of your Lambda. You’ll notice I added the Context object already in the sample above, but didn’t use it.

One Step Further: Logs

Lastly, if you want to make your life easier to find the CloudWatch logs specific to this error, you can add a call to the CloudWatchLogs sdk and create a new log stream. This way, when your alarm is triggered you don’t have to go searching among all the successful invocations of your Lambda in CloudWatch to find the bad needle in the haystack associated with this error. Here’s the code and I’ll include the use of the Lambda’s Context object to make it dynamic for the function name:

At the top of your js file add:

const cloudwatchlogs = new AWS.CloudWatchLogs();
const sendToCloudWatch(context, error){

Then inside the monitoring function add:

const {functionName} = context;
const logStreamName = `${Date.now()} ${functionName}`
await cloudwatchlogs.createLogStream({ logGroupName: <logGroupName>, logStreamName: <logStreamName>}).promise();const logParams = {
logEvents: [
{
message: JSON.stringify(error),
timestamp: Date.now()
}
],
logGroupName: <logGroupName>,
logStreamName: <logStreamName>
}
await cloudwatchlogs.putLogEvents(logParams).promise();

This will create a new log stream and log each time it’s run. I would prefer for the SDK to allow us more easily to stick these log events in the same error stream per Lambda, but there is a key you need to put multiple logs in a single stream. So, for the sake of simplicity I create a new stream each time.

I pass the “error” of the Catch as the message of the logEvent, so be sure to add that as a parameter of the monitoring function. This is comparable to the information you already see in the CloudWatch logs.

<logGroupName> is the parent container of all of the streams. You could make this static, as I do, so that you have only log group to look within to find errors. Alternatively, use that Context object and dynamically set the log group name accordingly.

So there you have it. We can have the benefit of Try/Catch blocks in our Node.js Lambda, allowing us to error without abruptly ending our code, and the benefits of the CloudWatch monitoring to let us know when the Catches are triggered.

Hope you enjoyed!

--

--

Luke Miller

Working towards master-status for all things front-end web development