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.
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:
async
before function
means that this is an async function.
await
operator is applied to Promises and either extracts fulfillment values or throws rejection values. More on it soon.
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',
});
});
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.
await
operator makes Promises synchronousInside 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:
If the Promise is fulfilled, await
returns the fulfillment value.
async function f() {
assert.equal(
await Promise.resolve('fulfilled'),
'fulfilled'
);
}
If the Promise is rejected, await
throws the rejection value.
async function f() {
try {
await Promise.reject('rejected');
} catch (err) {
assert.equal(err, 'rejected');
}
}
The result of an async function is always a Promise:
Any value that is returned (explicitly or implicitly) is used to resolve that Promise:
async function f1() { return 'fulfilled' }
f1().then(
result => assert.equal(result, 'fulfilled')
);
Any exception that is thrown is used to reject the Promise:
async function f() { throw 'rejected' }
f().catch(
error => assert.equal(error, 'rejected')
);
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).
That being said: These two terms are also often used interchangeably.
Exercise: Fetch API via async functions
exercises/async-functions/fetch_json2_test.mjs
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:
await
returns the fulfillment value.
await
throws the rejection value.
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.
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”.
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!'));
}
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
await
be used?await
at the top levels of modules ES2022We 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).
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).
.map()
with an async function as a callbackWhat 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']
);
Exercise: Mapping and filtering asynchronously
exercises/async-functions/map_async_test.mjs
return
in async functionsIf 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!'));
});
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:
return q
inside promise.then((result) => { ··· })
return q
inside promise.catch((err) => { ··· })
resolve(q)
inside new Promise((resolve, reject) => { ··· })
Async functions are executed as follows:
resultPromise
for the result is created when the async function is started.
resultPromise
is settled:
return
resolves resultPromise
.
throw
rejects resultPromise
.
await
whose operand is a Promise p
:
yield
works in sync generators).
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
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.
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
:
try-catch
statement (see below).
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');
});
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);
});
}
await
: running Promise-based functions sequentiallyIf 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.
await
: running Promise-based functions concurrentlyIf 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