How to use ES6 modules and top-level await in AWS Lambda

ESM and top-level await are now possible for Lambda functions

Author's image
Tamás Sallai
3 mins

Modules in Lambda

The Node14 Lambda runtime recently got support to ES6 modules and top-level await. Using them, the handler can support more modern structures.

Previously, the handler had to use CommonJS:

// commonjs

// import is via require
const dep = require("./dep.js");

// export is with module.exports
module.exports.handler = async (event) => {
	return "success";
};

Now it can use ESM:

// esm

// import
import dep from "./dep.js";

// export
export const handler = async (event) => {
	return "success";
};

This is great news, as the whole JS ecosystem is moving towards native modules and away from CommonJS. For example, node-fetch v3 is ESM-only and updating to that broke Lambda handlers. In the future, it is expected that more packages will follow this trend, so it was about time Lambda added support.

Let's see how to start using ES6 modules in your Lambda functions!

package.json

If you deploy with a package.json, all you need to do is add "type": "module":

{
	"..."
	"type": "module",
	"..."
}

This marks the whole package as ESM-compatible. If the index.js has an exported handler function, the Lambda handler will be index.handler.

mjs

If you don't have a package.json or you don't want to turn the whole code to ES6 modules, you can take advantage of the mjs file extension. This marks that single file as a module, without changing anything for other files.

The Lambda handler will be index.handler as before. The runtime will pick up the mjs file.

Top-level await

With ES6 modules, Lambda also got support for top-level await. This is when you can use the await keyword outside the handler function, which makes it easier to do complex initialization before the first run.

For example, you can load a value even if it needs network requests:

const value = await loadValue();

export const handler = async () => {
	// use value
}

If you use it, keep in mind that initialization runs only once per instance, and if there is a steady load that instance might be running for a long time. If you read a value outside the handler then it won't pick up any changes. For example, if you read a parameter from SSM, the value might be read once and if you change it the function will still use the old value.

Because of this, I recommend using a time-based cache that guarantees that the function refetches the value from time to time.

February 1, 2022
In this article