JavaScript for impatient programmers (ES2022 edition)
Please support this book: buy it or donate
(Ad, please don’t block.)

41 Async functions



Roughly, async functions provide better syntax for code that uses Promises. In order to use async functions, we should therefore understand Promises. They are explained in the previous chapter.

41.1 Async functions: the basics

Consider the following async function:

async function fetchJsonAsync(url) {
  try {
    const request = await fetch(url); // async
    const text = await request.text(); // async
    return JSON.parse(text); // sync
  }
  catch (error) {
    assert.fail(error);
  }
}

The previous, rather synchronous-looking code is equivalent to the following code that uses Promises directly:

function fetchJsonViaPromises(url) {
  return fetch(url) // async
  .then(request => request.text()) // async
  .then(text => JSON.parse(text)) // sync
  .catch(error => {
    assert.fail(error);
  });
}

A few observations about the async function fetchJsonAsync():

Both fetchJsonAsync() and fetchJsonViaPromises() are called in exactly the same way, like this:

fetchJsonAsync('http://example.com/person.json')
.then(obj => {
  assert.deepEqual(obj, {
    first: 'Jane',
    last: 'Doe',
  });
});

  Async functions are as Promise-based as functions that use Promises directly

From the outside, it is virtually impossible to tell the difference between an async function and a function that returns a Promise.

41.1.1 Async constructs

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

// Async function declaration
async function func1() {}

// Async function expression
const func2 = async function () {};

// Async arrow function
const func3 = async () => {};

// Async method definition in an object literal
const obj = { async m() {} };

// Async method definition in a class definition
class MyClass { async m() {} }

  Asynchronous functions vs. async functions

The difference between the terms asynchronous function and async function is subtle, but important:

41.2 Returning from async functions

41.2.1 Async functions always return Promises

Each async function always returns a Promise.

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

async function asyncFunc() {
  return 123; // (A)
}

asyncFunc()
.then(result => {
  assert.equal(result, 123);
});

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

async function asyncFunc() {
}

asyncFunc()
.then(result => {
  assert.equal(result, undefined);
});

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

async function asyncFunc() {
  throw new Error('Problem!'); // (A)
}

asyncFunc()
.catch(err => {
  assert.deepEqual(err, new Error('Problem!'));
});

41.2.2 Returned Promises are not wrapped

If we 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.

async function asyncFunc() {
  return Promise.resolve('abc');
}

asyncFunc()
.then(result => assert.equal(result, 'abc'));

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

41.2.3 Executing async functions: synchronous start, asynchronous settlement (advanced)

Async functions are executed as follows:

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).

async function asyncFunc() {
  console.log('asyncFunc() starts'); // (A)
  return 'abc';
}
asyncFunc().
then(x => { // (B)
  console.log(`Resolved: ${x}`);
});
console.log('Task ends'); // (C)

// Output:
// 'asyncFunc() starts'
// 'Task ends'
// 'Resolved: abc'

41.3 await: working with Promises

The await operator can only be used inside async functions and async generators (which are explained in §42.2 “Asynchronous generators”). Its operand is usually a Promise and leads to the following steps being performed:

Read on to find out more about how await handles Promises in various states.

41.3.1 await and fulfilled Promises

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

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

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

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

41.3.2 await and rejected Promises

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

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

  Exercise: Fetch API via async functions

exercises/async-functions/fetch_json2_test.mjs

41.3.3 await is shallow (we can’t use it in callbacks)

If we are inside an async function and want to pause it via await, we must do so directly within that function; we 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:

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

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

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

async function downloadContent(urls) {
  return urls.map(async (url) => {
    return await httpGet(url);
  });
}

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:

async function downloadContent(urls) {
  const promiseArray = urls.map(async (url) => {
    return await httpGet(url); // (A)
  });
  return await Promise.all(promiseArray);
}

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

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

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

41.3.4 Using await at the top levels of modules [ES2022]

We can use await at the top levels of modules – for example:

let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}

For more information on this feature, see §27.14 “Top-level await in modules [ES2022]”.

  Exercise: Mapping and filtering asynchronously

exercises/async-functions/map_async_test.mjs

41.4 (Advanced)

All remaining sections are advanced.

41.5 Concurrency and await

In the next two subsections, we’ll use the helper function paused():

/**
 * Resolves after `ms` milliseconds
 */
function delay(ms) {
  return new Promise((resolve, _reject) => {
    setTimeout(resolve, ms);
  });
}
async function paused(id) {
  console.log('START ' + id);
  await delay(10); // pause
  console.log('END ' + id);
  return id;
}

41.5.1 await: running asynchronous functions sequentially

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

async function sequentialAwait() {
  const result1 = await paused('first');
  assert.equal(result1, 'first');
  
  const result2 = await paused('second');
  assert.equal(result2, 'second');
}

// Output:
// 'START first'
// 'END first'
// 'START second'
// 'END second'

That is, paused('second') is only started after paused('first') is completely finished.

41.5.2 await: running asynchronous functions concurrently

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

async function concurrentPromiseAll() {
  const result = await Promise.all([
    paused('first'), paused('second')
  ]);
  assert.deepEqual(result, ['first', 'second']);
}

// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'

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 §40.6.2 “Concurrency tip: focus on when operations start” that what counts is when we start a Promise-based computation; not how we process its result. Therefore, the following code is as “concurrent” as the previous one:

async function concurrentAwait() {
  const resultPromise1 = paused('first');
  const resultPromise2 = paused('second');
  
  assert.equal(await resultPromise1, 'first');
  assert.equal(await resultPromise2, 'second');
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'

41.6 Tips for using async functions

41.6.1 We don’t need await if we “fire and forget”

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

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

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.

Note: Each invocation of .write() starts synchronously. That prevents race conditions.

41.6.2 It can make sense to await and ignore the result

It can occasionally make sense to use await, even if we ignore its result – for example:

await longRunningAsyncOperation();
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.