How does AWS Lambda invoke your Node.js function

Have you ever wondered how AWS Lambda invokes your custom Node.js function, purely in the context of JavaScript? Me neither, but after looking at the all possible ways you can define your function it sparked my interest to find out.

So in Lambda, let’s have our function defined as

exports.myHandler = function(event, context) {
  console.log("Event: " + JSON.stringify(event))
  return 'this is the result';
}

File: myIndex.js

When requested to run this function in the Node.js runtime, node is invoked. But is it called directly on your function? No, there is wrapped that deals with the internals of Lambda.

Very roughly this wrapper does something like

...
// somewhere we construct event and context

const task = require('./task/myIndex');
const result = task.myHandler(event, context);
...

File: wrapper.js

OK, but Lambda allows me to name my file and my handler anyway I like, how is this handled? This is where the handler property comes in hand. It is a property on the function definition that is used to specify the

The module-name.export value in your function. For example, “index.handler” calls exports.handler in index.js

In our example, our handler should be “myIndex.myHandler”.

And how would the wrapping code handle any handler names –

...
// somewhere we construct event and context

const [fileName, handlerName] = handler.split('.');
// i.e. fileName = handler.split('.')[0]; handlerName = handler.split('.')[1];
const task = require('./task/' + fileName);
const result = task[handlerName](event, context);
...

File: wrapper.js

That is nice, but what is even nicer is that Lambda allows us to define our function in three main formats:

1. Synchronous function that returns the result directly

exports.myHandler = function(event, context) {
  return 'this is the result';
}

2. Synchronous/Asynchronous function that returns a promise

exports.myHandler = function(event, context) {
  return new Promise((res, rej) => {
    // do any asynchronous operations if needed...
    res('this is the result');
  });
}

also

exports.myHandler = async function(event, context) {
  // do any asynchronous operations if needed...
  return 'this is the result';
}

3. Synchronous/Asynchronous function that uses a callback

exports.myHandler = async function(event, context, callback) {
  // do any asynchronous operations if needed...
  // first argument is for error if any
  callback(null, 'this is the result');
}

All of those are covered in detail in AWS Lambda Function Handler in Node.js
That is really nice because it allows for different coding styles, not enforcing any single once specifically.  And it is quite easy to handle, to be honest:

...
// somewhere we construct event and context
// somewhere we define how the wrapper treats the results and errors via a callback

const [fileName, handlerName] = handler.split('.');
// i.e. fileName = handler.split('.')[0]; handlerName = handler.split('.')[1];
const task = require('./task/' + fileName);
const result = task[handlerName](event, context, callback);

if (result) {
  if (isPromise(result)) {
     // case 2.
     // the result is a promise, i.e. an object with a function then and/or catch
     result.then(res => callback(null, res)).catch(err => callback(err));
  } else {
    // case 1.
    // the result from the synchronous function invocation
    callback(null, result);
  }
} else {
  // case 3
  // the passed callback as a third optional argument will be used when the function is ready to return the result/error
}

File: wrapper.js

Of course there are also other interesting things like how the event and context are constructed, how the result and error are processed. In this post we have covered only the actual function invocation.

Note that actual implementation used by Lambda may be different as this is based only on my findings from some reverse engineering/imagination.

Leave a comment