How can I use async/await at the top level?

Top-Level await has moved to stage 3, so the answer to your question How can I use async/await at the top level? is to just use await:

const text = await Promise.resolve('Hey there');
console.log('outside: ' + text)

Of if you want a main() function: add await to the call to main() :

async function main() {
    var value = await Promise.resolve('Hey there');
    console.log('inside: ' + value);
    return value;
}

var text = await main();  
console.log('outside: ' + text)

Compatibility

  • v8 since Oct 2019
    • the REPL in Chrome DevTools, Node.js and Safari web inspector
  • Node v13.3+ behind the flag --harmony-top-level-await
  • TypeScript 3.8+ (issue)
  • Deno since Oct 2019
  • [email protected]

I can't seem to wrap my head around why this does not work.

Because main returns a promise; all async functions do.

At the top level, you must either:

  1. Use top-level await (proposal, MDN; ES2022, broadly supported in modern environments) that allows top-level use of await in a module.

    or

  2. Use a top-level async function that never rejects (unless you want "unhandled rejection" errors).

    or

  3. Use then and catch.

#1 top-level await in a module

You can use await at the top-level of a module. Your module won't finish loading until the promise you await settles (meaning any module waiting for your module to load won't finish loading until the promise settles). If the promise is rejected, your module will fail to load. Typically, top-level await is used in situations where your module won't be able to do its work until the promise is settled and won't be able to do it at all unless the promise is fulfilled, so that's fine:

const text = await main();
console.log(text);

If your module can continue to work even if the promise is rejected, you could wrap the top-level await in a try/catch:

// In a module, once the top-level `await` proposal lands
try {
    const text = await main();
    console.log(text);
} catch (e) {
    // Deal with the fact the chain failed
}
// `text` is not available here

when a module using top-level await is evaluated, it returns a promise to the module loader (like an async function does), which waits until that promise is settled before evaluating the bodies of any modules that depend on it.

You can't use await at the top level of a non-module script, only in modules.

#2 - Top-level async function that never rejects

(async () => {
    try {
        const text = await main();
        console.log(text);
    } catch (e) {
        // Deal with the fact the chain failed
    }
    // `text` is not available here
})();
// `text` is not available here, either, and code here is reached before the promise settles
// and before the code after `await` in the main function above runs

Notice the catch; you must handle promise rejections / async exceptions, since nothing else is going to; you have no caller to pass them on to (unlike with #1 above, where your "caller" is the module loader). If you prefer, you could do that on the result of calling it via the catch function (rather than try/catch syntax):

(async () => {
    const text = await main();
    console.log(text);
})().catch(e => {
    // Deal with the fact the chain failed
});
// `text` is not available here, and code here is reached before the promise settles
// and before the code after `await` in the main function above runs

...which is a bit more concise, though it somewhat mixes models (async/await and explicit promise callbacks), which I'd normally otherwise advise not to.

Or, of course, don't handle errors and just allow the "unhandled rejection" error.

#3 - then and catch

main()
    .then(text => {
        console.log(text);
    })
    .catch(err => {
        // Deal with the fact the chain failed
    });
// `text` is not available here, and code here is reached before the promise settles
// and the handlers above run

The catch handler will be called if errors occur in the chain or in your then handler. (Be sure your catch handler doesn't throw errors, as nothing is registered to handle them.)

Or both arguments to then:

main().then(
    text => {
        console.log(text);
    },
    err => {
        // Deal with the fact the chain failed
    }
);
// `text` is not available here, and code here is reached before the promise settles
// and the handlers above run

Again notice we're registering a rejection handler. But in this form, be sure that neither of your then callbacks throws any errors, since nothing is registered to handle them.