Write it before the article
This post, translated from ASYNC/AWAIT WILL MAKE YOUR CODE SIMPLER, was written in August 2017 and nominated by a column as one of the 10 must-read posts of 2017. I couldn’t find the translation of this article on the nuggets, so I thought I’d try to translate it myself. Translation of the bad place, but also hope you pointed out, for my level of good do not question the level of the Nuggets (last article comments on the bitter ~ ▽ ~), thank you.
Async/Await makes your code more concise
Or, how do I learn not to use callbacks and love ES8
Sometimes, modern JavaScript projects get out of our hands. One of the main culprits is messy processing of asynchronous tasks, resulting in long, complex and deeply nested blocks of code. JavaScript now provides a new syntax for handling these operations, which can turn even the most intricate operations into concise and readable code
background
AJAX (Asynchronous JavaScript And XML)
Let’s start with a bit of popular science. In the late ’90s, Ajax was the first major breakthrough in asynchronous JavaScript. This technique allows web sites to retrieve and display new data after the HTML has been loaded. It was a revolutionary innovation for most websites at the time, which required redownloading the entire page to display a section of content. This technology (best known as the helper function bundle in jQuery) dominated Web development throughout the 21st century, and Ajax is still the primary technology that websites use to retrieve data today, but XML has been massively replaced by JSON
NodeJS
When NodeJS was first released in 2009, one of the main concerns on the server side was to allow applications to gracefully handle concurrency. At the time, most server-side languages used blocking code completion to process I/O operations until it finished processing I/O operations and then continued to run the previous code. Instead, NodeJS uses an event loop system that works in an Ajax-like way: once the non-blocking asynchronous operation is complete, the developer-assigned callback function can be triggered.
Promises
A few years later, a new standard called “Promises” emerged in NodeJS and browser environments that provided a more powerful and standardized way to build asynchronous operations. Promises still uses a callback-based format, but provides a uniform syntax for chain calls and builds for asynchronous operations. Promises, a standard created by popular open source libraries, was finally added to native JavaScript in 2015.
Promises are a major improvement, but they still result in long and unreadable code in some cases.
Now, we have a new solution.
Async /await is a new syntax (borrowed from.net and c#) that allows us to build promises like ordinary functions without callbacks. This is a nifty JavaScript addition that was added to JavaScript ES7 last year, and it can even be used to simplify almost any existing JS application.
Examples
We’ll give you a couple of examples.
These code examples do not require loading any tripartite libraries. **Async/await is already fully supported in the latest versions of Chrome, Firefox, Safari, and Edge, so you can try running these examples in the browser console. ** In addition, async/await syntax can run on Node 7.6 or later, and Babel and TypeScript also support async/await syntax. Async and await are now fully usable in any JavaScript project
Setup
If you want to follow in our footsteps and explore Async on your computer, we will use this virtual API Class. This class simulates the network call by returning promise objects that will be passed simple data as arguments using the resolve function 200ms after the call.
class Api {
constructor () {
this.user = { id: 1, name: 'test' }
this.friends = [ this.user, this.user, this.user ]
this.photo = 'not a real photo'
}
getUser () {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.user), 200)
})
}
getFriends (userId) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.friends.slice()), 200)
})
}
getPhoto (userId) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.photo), 200)
})
}
throwError () {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Intentional Error')), 200)})}}Copy the code
Each example will perform the same three actions in order: retrieve a user, retrieve their friends, and retrieve their photos. Finally, we will print the above three results on the console.
First try – nested promise callback function
Here is an implementation using nested promise callback functions
function callbackHell () {
const api = new Api()
let user, friends
api.getUser().then(function (returnedUser) {
user = returnedUser
api.getFriends(user.id).then(function (returnedFriends) {
friends = returnedFriends
api.getPhoto(user.id).then(function (photo) {
console.log('callbackHell', { user, friends, photo })
})
})
})
}
Copy the code
This is probably all too familiar to any JavaScript user. This code block has a very simple purpose, is long and nested at a high level, and ends with a bunch of parentheses
})})})}Copy the code
In a real code base, each callback function could be quite long, which could lead to some very verbose and high-level nested functions. We call this code for using callbacks within callbacks “callback hell.”
Worse, there is no way to check for errors, so any callback could cause an undetected failure as an unhandled Promise rejection.
Second attempt – chain promise
Let’s see if we can improve on that
function promiseChain () {
const api = new Api()
let user, friends
api.getUser()
.then((returnedUser) => {
user = returnedUser
return api.getFriends(user.id)
})
.then((returnedFriends) => {
friends = returnedFriends
return api.getPhoto(user.id)
})
.then((photo) => {
console.log('promiseChain', { user, friends, photo })
})
}
Copy the code
A nice feature of promises is their ability to chain by returning another Promise object within each callback. This approach treats all callbacks as horizontal. In addition, we can use the arrow function to abbreviate the callback expression.
This variant is significantly more readable than the previous one and has a good sense of sequence. Unfortunately, however, it’s still long and seems a bit complicated
The third one tries Async/Await
Is it possible that we don’t use any callbacks? Impossible? Ever think of the possibility of implementing it in just seven lines?
async function asyncAwaitIsYourNewBestFriend () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const photo = await api.getPhoto(user.id)
console.log('asyncAwaitIsYourNewBestFriend', { user, friends, photo })
}
Copy the code
Is it getting better? Calling await before the promise suspends the function flow until the Promise is in the Resolved state, and then assigns the result to the variable to the left of the equals sign. This approach allows us to write an asynchronous operation flow that behaves like a normal synchronous command.
I think you’re as excited about this feature as I am right now? !
Note that the keyword “async” is declared at the beginning of the entire function declaration. We have to do this, because it actually turns the whole function into a promise. We’ll look at that later.
LOOPS (cycle)
Async/await makes it very easy to perform previously complex operations, such as adding a list of friends that we want to retrieve for each user in order.
First attempt – recursive promise loop
Here’s how to get each friend list in order, which might look like a plain old promise.
function promiseLoops () {
const api = new Api()
api.getUser()
.then((user) => {
return api.getFriends(user.id)
})
.then((returnedFriends) => {
const getFriendsOfFriends = (friends) => {
if (friends.length > 0) {
let friend = friends.pop()
return api.getFriends(friend.id)
.then((moreFriends) => {
console.log('promiseLoops', moreFriends)
return getFriendsOfFriends(friends)
})
}
}
return getFriendsOfFriends(returnedFriends)
})
}
Copy the code
We created an internal function to call back promises to get friends of friends until the list is empty. O__O We did implement the functionality and it was great, but we actually used a very complex solution to solve a fairly simple task.
Note – using promise.all() to try to simplify the PromiseLoops() function causes it to behave as a function with a completely different function. The purpose of this code snippet is to run the operations sequentially (one after the other), but promise.all runs all asynchronous operations simultaneously (all at once). However, it is worth emphasizing that Async/await is still very powerful in combination with promise.all (), as we will show in the next section.
Second attempt – Async/Await for loop
This might be quite simple.
async function asyncAwaitLoops () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
for (let friend of friends) {
let moreFriends = await api.getFriends(friend.id)
console.log('asyncAwaitLoops', moreFriends)
}
}
Copy the code
You don’t have to write any recursive promises, just a for loop. See, this is your life friend Async/Await
PARALLEL OPERATIONS
Fetching each friend list one at a time seems slow, so why not do it in parallel? Can we use async/await to implement this requirement?
Obviously, you can. Your friend it can solve any problem. 🙂
async function asyncAwaitLoopsParallel () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const friendPromises = friends.map(friend => api.getFriends(friend.id))
const moreFriends = await Promise.all(friendPromises)
console.log('asyncAwaitLoopsParallel', moreFriends)
}
Copy the code
To run these operations in parallel, create a run promise array and pass it as an argument to promise.all (). It returns us a unique promise object that we can await, and this promise object will become resolved once all the operations have completed.
Error handling
However, this article has so far failed to address the important issue of asynchronous programming: error handling. The bane of many code bases is that asynchronous error handling often involves writing separate error handling callbacks for each operation. Because putting errors at the top of the call stack can be complicated, it is often necessary to explicitly check for errors thrown at the beginning of each callback. This method is tedious and error-prone. Furthermore, any exception thrown in a promise that is not properly caught will result in an undetected failure, leading to “invisible errors” in the code base due to incomplete error checking.
Let’s go back to the previous example and add error handling to each of these attempts. We will use an additional function, api.throwError(), to detect error handling before retrieving the user’s picture.
The first trial-promise error callback function
Let’s look at the worst:
function callbackErrorHell () {
const api = new Api()
let user, friends
api.getUser().then(function (returnedUser) {
user = returnedUser
api.getFriends(user.id).then(function (returnedFriends) {
friends = returnedFriends
api.throwError().then(function () {
console.log('Error was not thrown')
api.getPhoto(user.id).then(function (photo) {
console.log('callbackErrorHell', { user, friends, photo })
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}
Copy the code
That’s disgusting. In addition to being really long and ugly, the control flow is also very unintuitive because it comes in from the outside, rather than from the top down like normal readable code. Too bad. Let’s move on to the second attempt.
Second attempt – chained promise capture method
We can improve this by using a promise-catch combination (promise and catch, promise and catch).
function callbackErrorPromiseChain () {
const api = new Api()
let user, friends
api.getUser()
.then((returnedUser) => {
user = returnedUser
return api.getFriends(user.id)
})
.then((returnedFriends) => {
friends = returnedFriends
return api.throwError()
})
.then(() => {
console.log('Error was not thrown')
return api.getPhoto(user.id)
})
.then((photo) => {
console.log('callbackErrorPromiseChain', { user, friends, photo })
})
.catch((err) => {
console.error(err)
})
}
Copy the code
Much better than before, we can provide single error handling for all operations by using the single catch function at the end of the chained promise. However, it’s still a bit complicated, and we still have to use special callback functions to handle asynchronous errors rather than normal JavaScript errors.
Third attempt – normal try/catch block
We can do better.
async function aysncAwaitTryCatch () {
try {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
await api.throwError()
console.log('Error was not thrown')
const photo = await api.getPhoto(user.id)
console.log('async/await', { user, friends, photo })
} catch (err) {
console.error(err)
}
}
Copy the code
Here, we encapsulate the entire operation in a normal try/catch block. This way, we can throw and catch errors from synchronous code and one-step code in the same way. Obviously, it’s much simpler;)
Composition (combination)
I mentioned earlier that any function labeled async actually returns a Promise object. This allows us to compose asynchronous control flows very easily.
For example, we could reconfigure the previous examples to return user data rather than output it, and then retrieve the data by calling the async function as a Promise object.
async function getUserInfo () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const photo = await api.getPhoto(user.id)
return { user, friends, photo }
}
function promiseUserInfo () {
getUserInfo().then(({ user, friends, photo }) => {
console.log('promiseUserInfo', { user, friends, photo })
})
}
Copy the code
Better yet, we can also use the async/await syntax in the received function to produce a completely clean, understandable, and even refined block of asynchronous programming code.
async function awaitUserInfo () {
const { user, friends, photo } = await getUserInfo()
console.log('awaitUserInfo', { user, friends, photo })
}
Copy the code
What if we now needed to retrieve all the data for the first ten users?
async function getLotsOfUserData () {
const users = []
while (users.length < 10) {
users.push(await getUserInfo())
}
console.log('getLotsOfUserData', users)
}
Copy the code
What about concurrency? And rigorous error handling?
async function getLotsOfUserDataFaster () {
try {
const userPromises = Array(10).fill(getUserInfo())
const users = await Promise.all(userPromises)
console.log('getLotsOfUserDataFaster', users)
} catch (err) {
console.error(err)
}
}
Copy the code
Conclusion
With the rise of single-page JavaScript Web applications and the widespread adoption of NodeJS, elegantly handling concurrency is more important than ever for JavaScript developers. Async/Await relieves many of the bug-ridden problems that have plagued JavaScript code bases for decades due to control flow issues, and almost guarantees that any asynchronous block of code will be more refined, simpler, and more confident. And recently async/await is fully supported on almost all major browsers as well as NodeJS, so now is a good time to integrate these technologies into your code practices and projects.
The discussion time
Join the discussion on Reddit
Async /await makes your code simpler 1
Async /await makes your code simpler 2