Error handling in AWS Lambda
AWS Lambda is a service that lets you run code without provisioning or managing servers. That presents a great step forward in evolution of…
AWS Lambda is a service that lets you run code without provisioning or managing servers. That presents a great step forward in evolution of applications. But it is still a very young technology with immature environment and you have to deal with a bunch of basic problems. Especially if you come from web development in a language like PHP which, with all the available frameworks, can solve many tedious, basic and recurring tasks for you. One such task you have to solve in every new project is error handling.
Let’s take a lambda function which will login a user. We will use API Gateway with lambda proxy integration which puts request body string in event.body
and we need to pass output to the final callback in a specific format, see the docs. Simplified code could look like this.
module.exports.login = (event, context, callback) => {
validation.validate(event);
const body = JSON.parse(event.body);
return auth.login(body.email, body.password)
.then(res => callback(null, {
statusCode: 200,
body: JSON.stringify(res),
}));
};
It validates user input, authenticates the user in some external class, and returns a response to the final callback. It will work fine until the user passes malformed email address which will cause the validation method to throw an error or wrong password which will cause the login method to reject a promise. Lambda will output either nothing in the case of a rejected promise, or the error with its stack trace and API Gateway will show { "message": "Internal server error" }
because it didn’t get data in expected format. And even if you do not use lambda proxy integration, it will show something like
{
"errorMessage": "Unhandled rejection NotAuthorizedException: Incorrect username or password.",
"stackTrace": [
"at Request.extractError (/var/task/node_modules/aws-sdk/lib/protocol/json.js:43:27)",
"at Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:105:20)",
".."
]
}
But we certainly don’t want to show stack trace to our users, we simply want to tell them to fix the email or password. So, we need to ensure that all errors and rejected promises will be caught and transformed to some user friendly output. To accomplish this we encapsulate whole handler in a try/catch and add .catch()
block to the end of promises chain.
module.exports.login = (event, context, callback) => {
try {
validation.validate(event);
const body = JSON.parse(event.body);
return auth.login(body.email, body.password)
.then(res => callback(null, {
statusCode: 200,
body: JSON.stringify(res),
}))
.catch((err) => callback(null, {
statusCode: 400,
body: JSON.stringify({ "error": err.message }),
}));
} catch (err) {
callback(null, {
statusCode: 400,
body: JSON.stringify({ "error": err.message }),
});
}
};
We need to repeat this for every lambda function and we also should distinguish between user errors and unexpected app errors. The necessary code would grow quite long, so let’s improve it with few external functions.
const request = require('lib/request');
module.exports.login = (event, context, callback) => request.errorHandler(() => {
validation.validate(event);
const body = JSON.parse(event.body);
return request.responsePromise(
auth.login(body.email, body.password),
event,
context,
callback
);
}, event, context, callback);
The external module request.js
could look like:
const UserError = require('./UserError');
const request = module.exports;
request.errorHandler = function (fn, event, context, cb) {
try {
fn();
} catch (err) {
let res;
if (err instanceof UserError) {
res = request.getResponseBody(err, null, event, context);
if (typeof res.body === 'object') {
res.body = JSON.stringify(res.body);
}
} else {
res = {
statusCode: 500,
body: formatAppError(context),
};
}
cb(null, res);
}
};
const formatAppError = function (context) {
return {
errorMessage: 'Application error',
errorType: 'ApplicationError',
requestId: context.awsRequestId,
};
};
request.responsePromise = function (promise, event, context, callback) {
return promise
.then(res =>
request.response(null, res, event, context, callback)
)
.catch(err =>
request.response(err, null, event, context, callback)
);
};
request.response = function (err, res, event, context, cb) {
const response = {
statusCode: 200,
body: null,
};
if (err) {
if (err instanceof UserError) {
response.statusCode = typeof err.code === 'number' ? err.code : 400;
response.body = {
errorMessage: err.message,
errorType: err.type,
requestId: context.awsRequestId,
};
} else {
response.statusCode = 500;
response.body = formatAppError(context);
}
} else {
response.body = res;
}
response.body = response.body ? JSON.stringify(response.body) : '';
cb(null, response);
};
We use special class UserError
for differentiation between user error and application error. It is just an extended standard Error
class with added properties type
and code
. So when we catch a user error, we output its message and type and set its code as HTTP response status. Every other error is considered as application error and in that case we don't want to show its message to the user. In both cases we add AWS request id to the output so that our users can reference it if they need any help addressing the particular request.
Next time we will look how to log requests and how to get notified about unexpected errors.