Today I learned that top-level await in JavaScript REPLs, such as Chrome's Developer Tools console and Node.js REPL, is a BIG HACK!

Top-level await is a feature of JavaScript that allows you to use await outside of an async function, for example:

await Promise.resolve("cool")

Before it was added, the only way to use await was to wrap it in async function:

(async () => await Promise.resolve("cool"))()

However, top-level await only works within modules. This is a problem for REPLs, since they don't make a module for every expression that you type. They basically use eval() on it, running in the global scope: if you type x = 1 or var x = 1, you expect x to be a global variable.

So, await wouldn't work. But it does work! How? I initially thought they implemented some kind of a special eval-level await V8. Nope!

Turns out REPLs parse your expression and rewrite it into the async function! Both Node and Chrome use the acorn.js parser for that 🤯

If you type

await Promise.resolve('cool')

they turn it into

(async () => {return (await Promise.resolve('cool'));})()

What about global variables though? If you type var x = 1. and wrap it in a function, the var will be local to the function. But we want it to be global.

Here's the trick — they also rewrite variable definitions:

var x = 1; await Promise.resolve('ok');

turns into:

(async () => {void (x = 1); return (await Promise.resolve('ok'));})()

They strip var/let/const from what now are function-scoped variables and just assign values to the global. Notice that your const will not actually be a const, but that's fine, since REPLs allow redeclaring and reassigning top-level const and let anyway.

One funny thing: before running this whole parser stuff, Chrome dev tools check if rewriting is needed at all by looking for "async" in your code. So, if you type console.log("async work"), it will execute a tiny bit slower than console.log("meetings") 😜

If you want more details, read this processTopLevelAwait function in Node.