Roughly, async functions provide better syntax for code that uses Promises. In order to use async functions, you 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, 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:
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, 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:
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);
});
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:
resolve(q)
inside new Promise((resolve, reject) => { ··· })
return q
inside .then(result => { ··· })
return q
inside .catch(err => { ··· })
Async functions are executed as follows:
p
for the result is created when the async function is started.p
:
return
fulfills p
.throw
rejects p
.q
via await
. The async function is paused and execution leaves it. It is resumed once q
is settled.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).
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 PromisesThe await
operator can only be used inside async functions and async generators (which are explained in §39.2 “Asynchronous generators”). 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:
Non-Promise values are allowed, too, and simply passed on (synchronously, without pausing the async function):
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);
}
Instances of Error
(including 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.mjs
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 directly 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.mjs
All remaining sections are advanced.
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:
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 you 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 §37.5.2 “Concurrency tip: focus on when operations start” 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 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 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 you only want to 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.
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 you ignore its result. For example:
Here, we are using await
to join a long-running asynchronous operation. That ensures that the logging really happens after that operation is done.