- Deeply Understanding JavaScript Async and Await with Examples
- By Arfat Salman
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: xionglong58
- Proofread by: Baddyo, Mcskiller, FireairForce
First, look at the next callback function. A callback function is executed at some point after it is called, otherwise just like any other normal function. Due to the asynchronous nature of JavaScript, callbacks are used wherever the return value of the function is not immediately available.
Here is an example of node.js reading a file (asynchronous operation) —
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
Copy the code
The problem becomes apparent when we have to deal with multiple asynchronous operations. Suppose you have the following application scenario, where all operations are asynchronous
- Find the user in the database
Arfat
Read,profile_img_url
Data and then take the picture fromsomeServer.com
Download it. - After taking the image, we convert it to a different format, such as converting from PNG to JPEG.
- If the image format conversion is successful, the user is presented
Arfat
Send email. - Record the task in a file
transformations.log
Add a time stamp.
The code for the above process is roughly as follows
Note the nesting of callback functions and the hierarchy of program end}). Because of the similarity in structure, this approach is aptly called callback hell or callback pyramid. Some disadvantages of this approach are
- Having to understand the code from left to right makes it harder to read.
- Error handling becomes more complex and prone to error code.
To address these issues, JavaScript makes promises. Instead of nested callback functions, we can now use a chain structure. Here’s an example
One advantage of the callback process is that it has changed from a left-to-right structure to the familiar top-down structure. But promise still has some drawbacks
- We still have to be in every one of them
.then
Handles callbacks in. - Different from using
try/catch
We need to use.catch
Processing error. - Executing multiple promises sequentially in the body of a loop is challenging and unintuitive.
To prove the last drawback above, try the following challenge!
challenge
Suppose you want to print the numbers 0 to 10 in the for loop at arbitrary intervals (0 to n seconds). We’ll use promise to print 0 through 10 sequentially, for example, 0 takes 6 seconds to print, 1 takes 2 seconds to print, and 1 needs to print after 0 is complete, and other digital printing processes are similar.
Of course, don’t use async/await or.sort methods, we’ll fix this later.
Async function
The async function was introduced in ES2017 (ES8) to make it easier to apply promises.
- It is important to note that async functions are implemented based on promises.
- The concept of async/await is not entirely new.
- Async /await can be understood as an alternative to implementing asynchronous schemes based on promises.
- We can use async/await to avoid chain-calling promises.
- Async /await allows code to execute asynchronously while maintaining a normal, synchronous feel.
Therefore, you must understand promises before you can understand the concept of async/await.
grammar
Async /await contains two keywords async and await. Async is used to make functions run asynchronously. Async makes it possible to use the await keyword in functions, otherwise it is a syntax error to use await anywhere.
// Apply to a normal declared function asyncfunction myFn() { // await ... } // apply to arrow function const myFn = async () => {// await... }function myFn() {
// await fn(); (Syntax Error since no async)
}
Copy the code
Notice that the async keyword precedes the function declaration. In the arrow function, the async keyword is placed between = and parentheses.
Async functions can also be used as methods on objects or in classes as shown in the following code.
Const obj = {async as an object methodgetName() {
return fetch('https://www.example.com');
}
}
// 位于类中
class Obj {
async getResource() {
return fetch('https://www.example.com'); }}Copy the code
Note: Class constructors and getters/setters cannot be async functions.
Semantics and evaluation criteria
Async functions differ from normal JavaScript functions in the following ways
Async functions always return a Promise object.
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
Copy the code
The return value of fn, ‘hello’, is wrapped as a Promise object (implemented through promise.resolve) due to our use of the async keyword.
Therefore, an equivalent alternative that does not use the async keyword could be written —
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
Copy the code
In the code above we manually returned a Promise object to replace the async keyword.
Specifically, the return value of the async function will be passed to the promise.resolve method.
If the return value is an original value, promise.resolve returns a Promise version of that value. However, if the return value is a Promise object, promise.resolve will return that object intact.
Const p = promise.resolve (const p = promise.resolve)'hello')
p instanceof Promise;
// true// return promise.resolve (p) === p; //true
Copy the code
What happens when an async function throws an error?
For instance,
async function foo() {
throw Error('bar');
}
foo().catch(console.log);
Copy the code
If the error is not caught, the foo() function returns a promise with the status Rejected. Unlike promise.resolve, promise.reject wraps an error and returns. See error Handling later for details.
The end result is that no matter what result you want to return, you end up with a promise outside of async.
Async functions abort while await < expression > is being performed
The await command is like an expression. When await is followed by a promise, the async function encounters await and aborts until the corresponding Promise state becomes Resolved. Resolve becomes a Promise object with an resolved state when await is followed by the original value.
Const delayAndGetRandom = (ms) => {return new Promise(resolve => setTimeout(
() => {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
returna + b * c; } // execute the function fn fn().then(console.log);Copy the code
Let’s examine the function fn — line by line
-
When the function fn is called, const a = await 9 is executed first; . It is implicitly converted to const a = await promise.resolve (9); .
-
Since we are using await command, the fn function will pause at this point until variable A gets the value. The promise. resolve method returns a value of 9 in this case.
-
The delayAndGetRandom(1000) function causes the other programs in fn to pause until the delayAndGetRandom state changes to Resolved one second later. So, the execution of the fn function is effectively paused for 1 second.
-
In addition, the resolve function in delayAndGetRandom returns a random value. Whatever value is passed into resolve is assigned to variable B.
-
Again, the variable c is 5 and is delayed by another 1 second with await delayAndGetRandom(1000). We do not use the promise.resolve return value in this example.
-
Finally, we evaluate the result of a + b * c and wrap that result into a Promise with promise.resolve as the return value of the async function.
Note: If the pause and resume operations of the program above remind you of ES6’s Generator, that’s because generator has many advantages, too.
The solution
Let’s use async/await to solve the hypothetical problem mentioned above
We define an async function finishMyTask and await the results of queryDatabase, sendEmail, logTaskInFile operations with await.
If we compare the async/await solution with the one that uses promise, we see a similar amount of code. But async/await makes code simpler in syntactic complexity, without memorizing multiple layers of callback functions and.then /.catch.
Now let’s tackle the challenge of printing the numbers listed above. Here are two different solutions
const wait = (i, ms) = > new Promise(resolve= > setTimeout((a)= > resolve(i), ms));
// Method 1 (use for loop)
const printNumbers = (a)= > new Promise((resolve) = > {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) = > {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// Method two (using callback)
const printNumbersRecursive = (a)= > {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) = > {
console.log(val);
return processNextPromise(i + 1);
});
});
};
Copy the code
You can run the above code on the repl. It console.
This challenge is much easier to solve if you are allowed to use async functions.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000); console.log(i); }}Copy the code
Again, this method can be run on the repl. It Console.
Error handling
As we saw in the syntax section, an uncaught Error() is wrapped in a Rejected Promise. However, we can use try-catch to handle errors synchronously in async functions. Let’s start with this useful function
async function canRejectOrReturn() {// await new Promise(res =>setTimeout(res, 1000)); // There is a 50% chance of the Rejected stateif (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')}return 'perfect number';
}
Copy the code
CanRejectOrReturn () is an async function that can either return ‘perfect number’ or throw an error (‘Sorry, number too big’).
Let’s look at the sample code —
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'error caught'; }}Copy the code
Since we are waiting for the canRejectOrReturn function to execute, the promise in the canRejectOrReturn function will move to the Rejected state and throw an error, which will cause the catch block to be executed. Foo (rejected) returns undefined (because we did not return a value in the try) or ‘error caught’. Foo is never rejected because we use a try-catch in foo.
Here is an example of another version
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught'; }}Copy the code
Note that this time we return the function canRejectOrReturn from foo using return (instead of await). Foo is resolved and returns either ‘perfect number’ or Error(‘Sorry, number too big’). The catch block is never executed.
This is because foo returns the promise object returned by canRejectOrReturn. So Foo’s Resolved becomes canRejectOrReturn’s Resolved. You can equate return canRejectOrReturn() with the following two lines to understand (note that the first line is not await) —
try {
const promise = canRejectOrReturn();
}
Copy the code
Let’s look at await and return together —
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught'; }}Copy the code
In the example above, the foo function runs as resolved and returns either ‘perfect number’ or ‘error caught’. The result of foo will never be Rejected. This is just like the above example with only await. CanRejectOrReturn (rejected) is returned instead of undefined.
You can return await canRejectOrReturn(); Take it apart and see what it looks like
try {
const value = await canRejectOrReturn();
return value;
}
// ...
Copy the code
Common mistakes and pitfalls
Because of the intricacies involved between promise and async/await, subtle glitches may lurk in the program. Let’s take a look
The await keyword is not used
Sometimes we forget to use the await keyword before the promise object, or we forget to return the promise object. As shown below
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught'; }}Copy the code
Note that we are not using await or return. Foo will run as resolved with undefined and execute without a second delay. But the promise in canRejectOrReturn() does get implemented. This can happen if there are no side effects. If canRejectOrReturn () throws errors or state transition is rejected, UnhandledPromiseRejectionWarning error will be generated.
Use async functions in callbacks
We often refer to async functions as callbacks to.map or.filter methods. Let’s take an example – let’s say we have a function fetchPublicReposCount(username) that gets the number of public repositories owned by a Github user. We want to get the number of open repositories for three different users, so let’s look at the code —
const url = 'https://api.github.com/users'; Const fetchPublicReposCount = async (username) => {const response = await fetch('${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}
Copy the code
Number of public warehouses that want access to three users [‘ArfatSalman’, ‘Octocat ‘,’ Norvig ‘]. We might do something like this
const users = [
'ArfatSalman'.'octocat'.'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
Copy the code
Use ‘await’ too sequentially
Note that async is in the.map method. The number of public repositories that we might want the variable COUNTS to store. However, as we saw earlier, all async functions return promise objects. Therefore, counts is actually an array of Promise objects. .map calls the asynchronous function for each username, and the.map method stores the promise results returned by each call in an array.
We might have other solutions, like —
async function fetchAllCounts(users) {
const counts = [];
for (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}
Copy the code
We get each count manually and append them to the COUNTS array. The problem with the program is that the count of the second user can be fetched only after the first user’s count is fetched. Only one public warehouse number can be retrieved at a time.
If a FETCH operation takes 300 ms, the fetchAllCounts function takes about 900 ms. Thus, the program time increases linearly as the number of users increases. Because there is no dependency between obtaining the number of repositories exposed by different users, we can parallel the operations.
We can retrieve the user’s public repository number simultaneously, rather than sequentially. We’ll use the.map method and promise.all.
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}
Copy the code
Promise.all takes an array of Promise objects as input and returns a Promise object. When the states of all promise objects change to Resolved, the return value is an array of promise values corresponding to all promise values. As long as one promise object is rejected, Promise.all returns the value corresponding to the first Promise object rejected. However, running all promises at the same time might not work. Maybe you want to implement promises in bulk. Consider using p-Map for limited concurrency.
conclusion
Async functions become important. With the introduction of Async Iterators, Async functions will be more and more widely used. An in-depth understanding of async functions is essential for modern JavaScript developers. I hope this article has been enlightening. 🙂
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.