How AWS Lambda Executes Node.js Functions

Based on my question on StackOverflow I did a bit investigation about how (likely) are Node.js functions called in AWS Lambda containers.


First, I have to mention I don't have the actual implementation of the Node.js AWS Lambda container executors, so all following is only a simple thought experiment. But it seems to be very likely and can definitely help with understanding how are Node.js handler functions executed in AWS Lambda.

The problem as described in the question is following:

A simple function with an asynchrony inside is not correctly executed when the handler function is declared as async.

exports.handler = async (event, context, callback) => {
  setTimeout(() => callback(null, "resolved"), 100)
}

The result of the execution of this Lambda is null.

When the keyword async is removed from the handler everything works fine and the result is resolved as expected.

So, what is the difference?

According the documentation, the asynchronous handlers are executed without using the callback function and the result of the return is used instead.

I tried to simulate the executor code and came up with this:

var handler1 = async (event, context, callback) => {
  setTimeout(() => callback(null, "resolved"), 100)
}

var handler2 = async (event, context, callback) => {
  return "resolved"
}

var handler3 = (event, context, callback) => {
    setTimeout(() => callback(null, "resolved"), 100)
}

// main thread function
(async function main() {
  var handler = handler1 || handler2 || handler3 // set the handler 

  var call = () => new Promise(async (resolve, reject) => {
    var event = undefined
    var context = undefined
    
    if (isAsync(handler)) {
      try {
        // await the result
        var res = await handler(event, context, (err, succ) => {})
        resolve(res)        
      } catch (err) {
        reject(err)
      }      
    } else {
      // use the callback to get the result
      handler(event, context, function(err, succ) {
        if (err) reject(err)
        else resolve(succ)
      })
    }
  })

  var res = await call()
  console.log('RESULT', res || null)

})()

function isAsync(fn) {
   return fn.constructor.name === 'AsyncFunction'
}

When the handler is set to handler1 the result null is returned, when handler2 is used the result value is resolved as well as with handler3 - exactly the same as observed.

Final Thoughts

Well, the simulation above doesn't behave 100%, for example when there is no promising (like setTimeout(...)) in an async handler, the AWS Lambda executor will still take account of the callback function.

And this is exactly what I find so confusing - it's easy to overlook that and be suprised of the very new (and very wrong) results...

I wrote the simulation code to understand its behaviour as a rule of thumb - keep in mind:

Happy serverlessing!