HomepageExploring JavaScript (ES2025 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

44 Async functions ES2017

Async functions provide better syntax for code that uses Promises. Promises are therefore required knowledge for understanding async functions. They are explained in the previous chapter.

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

Two keywords are important:

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

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

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

Icon “details”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.

44.1.1 The await operator makes Promises synchronous

Inside the body of an async function, we write Promise-based code as if it were synchronous. We 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:

44.1.2 Returning a value from an async function resolves the function’s result

The result of an async function is always a Promise:

44.1.3 Asynchronous callable entities

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() {} }

Icon “details”Asynchronous functions vs. async functions

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

That being said: These two terms are also often used interchangeably.

Icon “exercise”Exercise: Fetch API via async functions

exercises/async-functions/fetch_json2_test.mjs

44.2 What values can be used with await?

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

For more information on what exactly pausing and resuming means, see “Async functions start synchronously, settle asynchronously” (§44.5).

Read on to find out how await handles various values.

44.2.1 Awaiting fulfilled Promises

If its operand is a fulfilled Promise, await returns its fulfillment value:

assert.equal(
  await Promise.resolve('fulfilled'), 'fulfilled'
);

The value of await is delivered asynchronously:

async function awaitPromise() {
  queueMicrotask( // (A)
    () => console.log('OTHER TASK')
  );
  console.log('before');
  await Promise.resolve('fulfilled');
  console.log('after');
}
await awaitPromise();

Output:

before
OTHER TASK
after

In line A, we can’t use setTimeout(). We have to use queueMicrotask() because Promise-related tasks are so-called microtasks which are different from normal tasks and always handled before them (via a microtask queue). For more information, see the MDN article “In depth: Microtasks and the JavaScript runtime environment”.

44.2.2 Awaiting rejected Promises

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

try {
  await Promise.reject(
    new Error('Problem!')
  );
  assert.fail(); // we never get here
} catch (err) {
  assert.deepEqual(err, new Error('Problem!'));
}

44.2.3 Awaiting non-Promise values

Non-Promise values can also be awaited and are simply passed on:

assert.equal(
  await 'non-Promise value', 'non-Promise value'
);

Even in this case, the result of await is delivered asynchronously:

async function awaitNonPromiseValue() {
  queueMicrotask(() => console.log('OTHER TASK'));
  console.log('before');
  await 'non-Promise value';
  console.log('after');
}
await awaitNonPromiseValue();

Output:

before
OTHER TASK
after

44.3 Where can await be used?

44.3.1 Using await at the top levels of modules ES2022

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

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

For more information on this feature, see “Top-level await in modules ES2022 (advanced)” (§29.15).

44.3.2 Awaiting is shallow

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.

Let’s examine what that means. In the following code, we try to await inside a nested function:

async function f() {
  const nestedFunc = () => {
    const result = await Promise.resolve('abc'); // SyntaxError!
    return 'RESULT: ' + result;
  };
  return [ nestedFunc() ];
}

However, that isn’t even valid syntax because await is not allowed inside synchronous functions such as nestedFunc(). What happens if we make nestedFunc() an async function?

async function f() {
  const nestedFunc = async () => {
    const result = await Promise.resolve('abc'); // (A)
    return 'RESULT: ' + result;
  };
  return [ nestedFunc() ]; // (B)
}
const arr = await f(); // (C)
assert.equal(
  arr[0] instanceof Promise, true
);

This time, the await in line A pauses nestedFunc(), not f(). nestedFunc() returns a Promise, which is wrapped in an Array in line B. Note the top-level await in line C.

To make this code work, we must await the result of nestedFunc():

async function f() {
  const nestedFunc = async () => {
    const result = await Promise.resolve('abc');
    return 'RESULT: ' + result;
  };
  return [ await nestedFunc() ];
}
assert.deepEqual(
  await f(), ['RESULT: abc']
);

To summarize: await only affects the immediately surrounding function (which must be an async function).

44.3.3 Example: .map() with an async function as a callback

What happens if we use an async function as a callback for .map()? Then the result is an Array of Promises:

const arrayOfPromises = arr.map(
  async (x) => { /*···*/ }
);

We can use Promise.all() to convert that Array of Promises to a Promise for an Array and await that Promise:

const array = await Promise.all(
  arr.map(
    async (x) => { /*···*/ }
  )
);

We use that technique in the following code, which downloads files via fetch(). The content of each file is its filename.

const urls = [
  'http://example.com/file1.txt',
  'http://example.com/file2.txt',
];
const uppercaseTexts = await Promise.all( // (A)
  urls.map(async (url) => {
    const response = await fetch(url);
    const text = await response.text();
    return text.toUpperCase();
  })
);
assert.deepEqual(
  uppercaseTexts,
  ['FILE1.TXT', 'FILE2.TXT']
);

Icon “exercise”Exercise: Mapping and filtering asynchronously

exercises/async-functions/map_async_test.mjs

44.4 return in async functions

44.4.1 The result of an async function is always a Promise

If we call an async function, the result is always a Promise – even if the async function throws an exception. Inside the async function, we can fulfill the result Promise by returning non-Promise values (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!'));
});

44.4.2 Returning a Promise resolves the result Promise

If we return a Promise q then it resolves the result Promise p of the async function: p adopts the state of q (q basically replaces p). Resolving never nests Promises.

Returning a fulfilled Promise fulfills the result Promise:

async function asyncFunc1() {
  return Promise.resolve('fulfilled');
}
const p1 = asyncFunc1();
p1.then(
  result => assert.equal(result, 'fulfilled')
);

Returning a rejected Promise has the same effect as throwing an exception:

async function asyncFunc2() {
  return Promise.reject('rejected');
}
const p2 = asyncFunc2();
p2.catch(
  error => assert.equal(error, 'rejected')
);

The behavior of return is similar to how a Promise q is treated in the following situations:

44.5 Async functions start synchronously, settle asynchronously

Async functions are executed as follows:

Note that the notification of the settlement of resultPromise 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

44.6 Tips for using async functions

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

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

44.6.3 The pros and cons of return await

If we await a Promise before returning it, we unwrap it before immediately wrapping it again:

async function f() {
  return await Promise.resolve('result');
}

Since return resolves the result Promise of f(), the following code is simpler and equivalent:

async function f() {
  return Promise.resolve('result');
}

There are, however, three reasons to stick with return await:

Let’s explore the last reason. If we await the rejected Promise in line A before returning it, it causes an exception:

async function f() {
  try {
    return await Promise.reject('error'); // (A)
  } catch (err) {
    return 'Caught an error: ' + err;
  }
}
f().then((result) => {
  assert.equal(result, 'Caught an error: error');
});

If, on the other hand, we return without await, no exception is thrown and the result Promise of f() adopts the state of the rejected Promise:

async function f() {
  try {
    return Promise.reject('error');
  } catch (err) {
    return 'Caught an error: ' + err;
  }
}
f().catch((reason) => {
  assert.equal(reason, 'error');
});

44.7 Concurrency and await (advanced)

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

async function returnAfterPause(id) {
  console.log('START ' + id);
  await delay(10); // pause
  console.log('END ' + id);
  return id;
}

/**
 * Resolves after `ms` milliseconds
 */
function delay(ms) {
  return new Promise((resolve, _reject) => {
    setTimeout(resolve, ms);
  });
}

44.7.1 await: running Promise-based functions sequentially

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

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

Output:

START first
END first
START second
END second

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

44.7.2 await: running Promise-based functions concurrently

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

async function concurrentPromiseAll() {
  const result = await Promise.all([
    returnAfterPause('first'),
    returnAfterPause('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 earlier 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 = returnAfterPause('first');
  const resultPromise2 = returnAfterPause('second');
  
  assert.equal(await resultPromise1, 'first');
  assert.equal(await resultPromise2, 'second');
}

Output:

START first
START second
END first
END second