280
votes

I have been going over async/await and after going over several articles, I decided to test things myself. However, I can't seem to wrap my head around why this does not work:

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

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

The console outputs the following (node v8.6.0) :

> outside: [object Promise]

> inside: Hey there

Why does the log message inside the function execute afterwards? I thought the reason async/await was created was in order to perform synchronous execution using asynchronous tasks.

Is there a way could I use the value returned inside the function without using a .then() after main()?

10
No, only time machines can make asynchronous code synchronous. await is nothing but sugar for promise then syntax.Bergi
Why does main return a value? If it should, probably it's not entry point and needs to be called by another function (e.g. async IIFE).Estus Flask
@estus it was just a quick function name while I was testing things in node, not necessarily representative of a program's mainFelipe
FYI, async/await is part of ES2017, not ES7 (ES2016)Felix Kling
For the interactive node shell (REPL), try node --experimental-repl-await.nomad

10 Answers

376
votes

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 a top-level async function that never rejects (unless you want "unhandled rejection" errors), or

  2. Use then and catch, or

  3. (Coming soon!) Use top-level await, a proposal that has reached Stage 3 in the process that allows top-level use of await in a module.

#1 - Top-level async function that never rejects

(async () => {
    try {
        var text = await main();
        console.log(text);
    } catch (e) {
        // Deal with the fact the chain failed
    }
})();

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. If you prefer, you could do that on the result of calling it via the catch function (rather than try/catch syntax):

(async () => {
    var text = await main();
    console.log(text);
})().catch(e => {
    // Deal with the fact the chain failed
});

...which is a bit more concise (I like it for that reason).

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

#2 - then and catch

main()
    .then(text => {
        console.log(text);
    })
    .catch(err => {
        // Deal with the fact the chain failed
    });

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
    }
);

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

#3 top-level await in a module

You can't use await at the top level of a non-module script, but the top-level await proposal (Stage 3) allows you to use it at the top level of a module. It's similar to using a top-level async function wrapper (#1 above) in that you don't want your top-level code to reject (throw an error) because that will result in an unhandled rejection error. So unless you want to have that unhandled rejection when things go wrong, as with #1, you'd want to wrap your code in an error handler:

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

Note that if you do this, any module that imports from your module will wait until the promise you're awaiting settles; when a module using top-level await is evaluated, it basically 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.

22
votes

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 add await 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)

Or just:

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

Compatibility

9
votes

To give some further info on top of current answers:

The contents of a node.js file are currently concatenated, in a string-like way, to form a function body.

For example if you have a file test.js:

// Amazing test file!
console.log('Test!');

Then node.js will secretly concatenate a function that looks like:

function(require, __dirname, ... perhaps more top-level properties) {
  // Amazing test file!
  console.log('Test!');
}

The major thing to note, is that the resulting function is NOT an async function. So you cannot use the term await directly inside of it!

But say you need to work with promises in this file, then there are two possible methods:

  1. Don't use await directly inside the function
  2. Don't use await

Option 1 requires us to create a new scope (and this scope can be async, because we have control over it):

// Amazing test file!
// Create a new async function (a new scope) and immediately call it!
(async () => {
  await new Promise(...);
  console.log('Test!');
})();

Option 2 requires us to use the object-oriented promise API (the less pretty but equally functional paradigm of working with promises)

// Amazing test file!
// Create some sort of promise...
let myPromise = new Promise(...);

// Now use the object-oriented API
myPromise.then(() => console.log('Test!'));

It would be interesting to see node add support for top-level await!

7
votes

You can now use top level await in Node v13.3.0

import axios from "axios";

const { data } = await axios.get("https://api.namefake.com/");
console.log(data);

run it with --harmony-top-level-await flag

node --harmony-top-level-await index.js

5
votes

The actual solution to this problem is to approach it differently.

Probably your goal is some sort of initialization which typically happens at the top level of an application.

The solution is to ensure that there is only ever one single JavaScript statement at the top level of your application. If you have only one statement at the top of your application, then you are free to use async/await at every other point everwhere (subject of course to normal syntax rules)

Put another way, wrap your entire top level in a function so that it is no longer the top level and that solves the question of how to run async/await at the top level of an application - you don't.

This is what the top level of your application should look like:

import {application} from './server'

application();
2
votes

Node -
You can run node --experimental-repl-await while in the REPL. I'm not so sure about scripting.

Deno -
Deno already has it built in.

1
votes

i like this clever syntax to do async work from an entrypoint

void async function main() {
  await doSomeWork()
  await doMoreWork()
}()
0
votes

Other solutions were lacking some important details for POSIX compliance:

You need to ...

  • Report a 0 exit status on success and non-zero on fail.
  • Emit errors to stderr output stream.
#!/usr/bin/env node

async function main() {
 // ... await stuff ... 
}

// POSIX compliant apps should report an exit status
main()
    .then(() => {
        process.exit(0);
    })
    .catch(err => {
        console.error(err); // Writes to stderr
        process.exit(1);
    });

If you're using a command line parser like commander, you may not need a main().

Example:

#!/usr/bin/env node

import commander from 'commander'

const program = new commander.Command();

program
  .version("0.0.1")
  .command("some-cmd")
  .arguments("<my-arg1>")
  .action(async (arg1: string) => {
    // run some async action
  });

program.parseAsync(process.argv)
  .then(() => {
    process.exit(0)
  })
  .catch(err => {
    console.error(err.message || err);
    if (err.stack) console.error(err.stack);
    process.exit(1);
  });
-1
votes

Top-level await is a feature of the upcoming EcmaScript standard. Currently, you can start using it with TypeScript 3.8 (in RC version at this time).

How to Install TypeScript 3.8

You can start using TypeScript 3.8 by installing it from npm using the following command:

$ npm install typescript@rc

At this time, you need to add the rc tag to install the latest typescript 3.8 version.

-5
votes

Since main() runs asynchronously it returns a promise. You have to get the result in then() method. And because then() returns promise too, you have to call process.exit() to end the program.

main()
   .then(
      (text) => { console.log('outside: ' + text) },
      (err)  => { console.log(err) }
   )
   .then(() => { process.exit() } )