Please support this book: buy it or donate

38. Async functions



Roughly, async functions provide better syntax for code that uses Promises.

38.1. Async functions: the basics

Consider the following async function:

  1. async function fetchJsonAsync(url) {
  2. try {
  3. const request = await fetch(url); // async
  4. const text = await request.text(); // async
  5. return JSON.parse(text); // sync
  6. }
  7. catch (error) {
  8. assert.fail(error);
  9. }
  10. }

The previous rather synchronous-looking code is equivalent to the following Promise-based code:

  1. function fetchJsonViaPromises(url) {
  2. return fetch(url) // async
  3. .then(request => request.text()) // async
  4. .then(text => JSON.parse(text)) // sync
  5. .catch(error => {
  6. assert.fail(error);
  7. });
  8. }

A few observations about the async function fetchJsonAsync():

  • Async functions are marked with the keyword async.

  • Inside the body of an async function, you write Promise-based code as if it were synchronous. You only need to apply the await operator whenever a value is a Promise. That operator pauses the async function and resumes it once the Promise is settled:

    • If the Promise is fulfilled, await returns the fulfillment value.
    • If the Promise is rejected, await throws the rejection value.
  • The result of an async function is always a Promise:

    • Any value that is returned (explicitly or implicitly) is used to fulfill the Promise.
    • Any exception that is thrown is used to reject the Promise.
      Both fetchJsonAsync() and fetchJsonViaPromises() are called in exactly the same way, like this:
  1. fetchJsonAsync('http://example.com/person.json')
  2. .then(obj => {
  3. assert.deepEqual(obj, {
  4. first: 'Jane',
  5. last: 'Doe',
  6. });
  7. });

38.1.1. Async constructs

JavaScript has the following async versions of synchronous callable entities. Their roles are always either real function or method.

  1. // Async function declaration
  2. async function func1() {}
  3. // Async function expression
  4. const func2 = async function () {};
  5. // Async arrow function
  6. const func3 = async () => {};
  7. // Async method definition (in classes, too)
  8. const obj = { async m() {} };

38.1.2. Async functions always return Promises

Each async function always returns a Promise.

Inside the async function, you fulfill the result Promise via return (line A):

  1. async function asyncFunc() {
  2. return 123; // (A)
  3. }
  4. asyncFunc()
  5. .then(result => {
  6. assert.equal(result, 123);
  7. });

As usual, if you don’t explicitly return anything, undefined is returned for you:

  1. async function asyncFunc() {
  2. }
  3. asyncFunc()
  4. .then(result => {
  5. assert.equal(result, undefined);
  6. });

You reject the result Promise via throw (line A):

  1. let thrownError;
  2. async function asyncFunc() {
  3. thrownError = new Error('Problem!');
  4. throw thrownError; // (A)
  5. }
  6. asyncFunc()
  7. .catch(err => {
  8. assert.equal(err, thrownError);
  9. });

38.1.3. Returned Promises are not wrapped

If you return a Promise p from an async function, then p becomes the result of the function (or rather, the result “locks in” on p and behaves exactly like it). That is, the Promise is not wrapped in yet another Promise.

  1. async function asyncFunc() {
  2. return Promise.resolve('abc');
  3. }
  4. asyncFunc()
  5. .then(result => assert.equal(result, 'abc'));

Recall that any Promise q is treated similarly in the following situations:

  • resolve(q) inside new Promise((resolve, reject) => { ··· })
  • return q inside .then(result => { ··· })
  • return q inside .catch(err => { ··· })

38.1.4. await: working with Promises

The await operator can only be used inside async functions. Its operand is usually a Promise and leads to the following steps being performed:

  • The current async function is paused (similar to how sync generators are paused when you yield).
  • Processing of the task queue continues.
  • Once the Promise is settled, the async function is resumed:
    • If the Promise is fulfilled, await returns the fulfillment value.
    • If the Promise is rejected, await throws the rejection value.
      The following two sections provide more details.

38.1.5. await and fulfilled Promises

If its operand ends up being a fulfilled Promise, await returns its fulfillment value:

  1. assert.equal(await Promise.resolve('yes!'), 'yes!');

Non-Promise values are allowed, too, and simply passed on (synchronously, without pausing the async function):

  1. assert.equal(await 'yes!', 'yes!');

38.1.6. await and rejected Promises

If its operand is a rejected Promise, then await throws the rejection value:

  1. try {
  2. await Promise.reject(new Error());
  3. assert.fail(); // we never get here
  4. } catch (e) {
  5. assert.equal(e instanceof Error, true);
  6. }

Instances of Error (which includes instances of its subclasses) are treated specially and also thrown:

  1. try {
  2. await new Error();
  3. assert.fail(); // we never get here
  4. } catch (e) {
  5. assert.equal(e instanceof Error, true);
  6. }

38.2. Terminology

Let’s clarify a few terms:

  • Async functions, async methods: are defined using the keyword async. Async functions are also called async/await, based on the two keywords that are their syntactic foundation.

  • Directly using Promises: means that code is handling Promises without await.

  • Promise-based: a function or method that delivers its results and errors via Promises. That is, both async functions and functions that return Promises, qualify.

  • Asynchronous: a function or method that delivers its results and errors asynchronously. Here, any operation that uses an asynchronous pattern (callbacks, events, Promises, etc.) qualifies. Alas, things are a bit confusing, because the “async” in “async function” is an abbreviation for “asynchronous”.

38.3. await is shallow (you can’t use it in callbacks)

If you are inside an async function and want to pause it via await, you must do so within that function, you can’t use it inside a nested function, such as a callback. That is, pausing is shallow.

For example, the following code can’t be executed:

  1. async function downloadContent(urls) {
  2. return urls.map((url) => {
  3. return await httpGet(url); // SyntaxError!
  4. });
  5. }

The reason is that normal arrow functions don’t allow await inside their bodies.

OK, let’s try an async arrow function, then:

  1. async function downloadContent(urls) {
  2. return urls.map(async (url) => {
  3. return await httpGet(url);
  4. });
  5. }

Alas, this doesn’t work, either: Now .map() (and therefore downloadContent()) returns an Array with Promises, not an Array with (unwrapped) values.

One possible solution is to use Promise.all() to unwrap all Promises:

  1. async function downloadContent(urls) {
  2. const promiseArray = urls.map(async (url) => {
  3. return await httpGet(url); // (A)
  4. });
  5. return await Promise.all(promiseArray);
  6. }

Can this code be improved? Yes it can, because in line A, we are unwrapping a Promise via await, only to re-wrap it immediately via return. We can omit await and then don’t even need an async arrow function:

  1. async function downloadContent(urls) {
  2. const promiseArray = urls.map(
  3. url => httpGet(url));
  4. return await Promise.all(promiseArray); // (B)
  5. }

For the same reason, we can also omit await in line B.

38.4. (Advanced)

All remaining sections are advanced.

38.5. Immediately invoked async arrow functions

If you need an await outside an async function (e.g., at the top level of a module), then you can immediately invoke an async arrow function:

  1. (async () => { // start
  2. const promise = Promise.resolve('abc');
  3. const value = await promise;
  4. assert.equal(value, 'abc');
  5. })(); // end

The result of an immediately invoked async arrow function is a Promise:

  1. const promise = (async () => 123)();
  2. promise.then(x => assert.equal(x, 123));

38.6. Concurrency and await

38.6.1. await: running asynchronous functions sequentially

If you prefix the invocations of multiple asynchronous functions with await, then those functions are executed sequentially:

  1. const otherAsyncFunc1 = () => Promise.resolve('one');
  2. const otherAsyncFunc2 = () => Promise.resolve('two');
  3. async function asyncFunc() {
  4. const result1 = await otherAsyncFunc1();
  5. assert.equal(result1, 'one');
  6. const result2 = await otherAsyncFunc2();
  7. assert.equal(result2, 'two');
  8. }

That is, otherAsyncFunc2() is only started after otherAsyncFunc1() is completely finished.

38.6.2. await: running asynchronous functions concurrently

If we want to run multiple functions concurrently, we need to resort to the tool method Promise.all():

  1. async function asyncFunc() {
  2. const [result1, result2] = await Promise.all([
  3. otherAsyncFunc1(),
  4. otherAsyncFunc2(),
  5. ]);
  6. assert.equal(result1, 'one');
  7. assert.equal(result2, 'two');
  8. }

Here, both asynchronous functions are started at the same time. Once both are settled, await gives us either an Array of fulfillment values or – if at least one Promise is rejected – an exception.

Recall from the previous chapter that what counts is when you start a Promise-based computation – not how you process its result. Therefore, the following code is as “concurrent” as the previous one:

  1. async function asyncFunc() {
  2. const promise1 = otherAsyncFunc1();
  3. const promise2 = otherAsyncFunc2();
  4. const result1 = await promise1;
  5. const result2 = await promise2;
  6. assert.equal(result1, 'one');
  7. assert.equal(result2, 'two');
  8. }

38.7. Tips for using async functions

38.7.1. Async functions are started synchronously, settled asynchronously

Async functions are executed as follows:

  • The Promise p for the result is created when the async function is started.
  • Then the body is executed. There are two ways in which execution can leave the body:
    • Execution can leave permanently, while settling p:
      • A return fulfills p.
      • A throw rejects p.
    • Execution can also leave temporarily, when awaiting the settlement of another Promise q via await. The async function is paused and execution leaves it. It is resumed once q is settled.
  • Promise p is returned after execution has left the body for the first time (permanently or temporarily).
    Note that the notification of the settlement of the result p happens asynchronously, as is always the case with Promises.

The following code demonstrates that an async function is started synchronously (line A), then the current task finishes (line C), then the result Promise is settled – asynchronously (line B).

  1. async function asyncFunc() {
  2. console.log('asyncFunc() starts'); // (A)
  3. return 'abc';
  4. }
  5. asyncFunc().
  6. then(x => { // (B)
  7. console.log(`Resolved: ${x}`);
  8. });
  9. console.log('Task ends'); // (C)
  10. // Output:
  11. // 'asyncFunc() starts'
  12. // 'Task ends'
  13. // 'Resolved: abc'

38.7.2. You don’t need await if you “fire and forget”

await is not required when working with a Promise-based function, you only need it if you want to pause and wait until the returned Promise is settled. If all you want to do, is start an asynchronous operation, then you don’t need it:

  1. async function asyncFunc() {
  2. const writer = openFile('someFile.txt');
  3. writer.write('hello'); // don’t wait
  4. writer.write('world'); // don’t wait
  5. await writer.close(); // wait for file to close
  6. }

In this code, we don’t await .write(), because we don’t care when it is finished. We do, however, want to wait until .close() is done.

38.7.3. It can make sense to await and ignore the result

It can occasionally make sense to use await, even if you ignore its result. For example:

  1. await longRunningAsyncOperation();
  2. console.log('Done!');

Here, we are using await to join a long-running asynchronous operation. That ensures that the logging really happens after that operation is done.