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.
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()
:
Async functions are marked with the keyword async
.
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:
await
returns the fulfillment value.
await
throws the rejection value.
The result of an async function is always a Promise:
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.
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:
An asynchronous function is any function that delivers its result asynchronously – for example, a callback-based function or a Promise-based function.
An async function is defined via special syntax, involving the keywords async
and await
. It is also called async/await due to these two keywords. Async functions are based on Promises and therefore also asynchronous functions (which is somewhat confusing).
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!'));
});
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:
resolve(q)
inside new Promise((resolve, reject) => { ··· })
return q
inside .then(result => { ··· })
return q
inside .catch(err => { ··· })
Async functions are executed as follows:
resultPromise
for the result is created when the async function is started.
resultPromise
is settled:
return
fulfills resultPromise
.
throw
rejects resultPromise
.
await
whose operand is a Promise p
:
p
is settled.
resultPromise
is returned after the first (permanent or temporary) exit.
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
await
: working with Promises
The await
operator can only be used inside async functions and async generators (which are explained in “Asynchronous generators” (§44.2)). Its operand is usually a Promise and leads to the following steps being performed:
yield
works in sync generators.
await
returns the fulfillment value.
await
throws the rejection value.
Read on to find out more about how await
handles Promises in various states.
await
and fulfilled PromisesIf 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!');
await
and rejected PromisesIf 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
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.
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” (§29.15).
Exercise: Mapping and filtering asynchronously
exercises/async-functions/map_async_test.mjs
All remaining sections are advanced.
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;
}
await
: running asynchronous functions sequentiallyIf 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.
await
: running asynchronous functions concurrentlyIf 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 “Concurrency tip: focus on when operations start” (§42.6.2) 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
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.
await
and ignore the resultIt 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.