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

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:

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 Promise-based code:

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

38.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 classes, too)
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):

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

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

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

async function asyncFunc() {
}

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

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

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

asyncFunc()
.catch(err => {
  assert.equal(err, thrownError);
});

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.

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:

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 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:

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!');

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

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

try {
  await 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.js

38.2. Terminology

Let’s clarify a few terms:

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:

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, 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:

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.

  Exercise: Mapping and filtering asynchronously

exercises/async-functions/map_async_test.js

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:

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

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

const promise = (async () => 123)();
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:

const otherAsyncFunc1 = () => Promise.resolve('one');
const otherAsyncFunc2 = () => Promise.resolve('two');

async function asyncFunc() {
  const result1 = await otherAsyncFunc1();
  assert.equal(result1, 'one');

  const result2 = await otherAsyncFunc2();
  assert.equal(result2, 'two');
}

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():

async function asyncFunc() {
  const [result1, result2] = await Promise.all([
    otherAsyncFunc1(),
    otherAsyncFunc2(),
  ]);
  assert.equal(result1, 'one');
  assert.equal(result2, 'two');
}

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:

async function asyncFunc() {
  const promise1 = otherAsyncFunc1();
  const promise2 = otherAsyncFunc2();

  const result1 = await promise1;
  const result2 = await promise2;

  assert.equal(result1, 'one');
  assert.equal(result2, 'two');
}

38.7. Tips for using async functions

38.7.1. Async functions are started synchronously, settled asynchronously

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'

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:

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.

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:

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.