Top-level await in JavaScript REPLs is a hack
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.