Introduction
I wrote about promises and generators being introduced in ES6. Another way to make asynchronous code look synchronous almost made it in ES6, but not quite: async/await. This functionality is built on top of promises. Let's take a look at it.
Syntax
The syntax is as follow: you must declare a function to be async
:
const asyncFunction = async () => {
// This is a good start
}
// or
const asyncFunction = async function(){
// Old school function keyword? I like it!
}
Then, inside this async
function, you can use the await
keyword to tell the function it should wait for something:
const asyncFunction = async () => {
const step1 = await fetchingData() // Wait for this
const step2 = await savingData() // Then wait for that
// Do something else
}
You can still keep your promises
I mentioned that async/await is build on top of promises. An async
function returns a promise. This means you can call .then() and .catch() on them:
const fs = require('fs')
// promisify is a neat tool in the util module that transforms a callback function into a promise one
const { promisify } = require('util')
const writeFile = promisify(fs.writeFile)
const readFile = promisify(fs.readFile)
const writeAndRead = async () => {
await writeFile('./test.txt', 'Hello World')
const content = await readFile('./test.txt', 'utf-8')
return content
}
writeAndRead()
.then(content => console.log(content)) // Hello World
Ok, what is happening here?
- We create an
async
function called writeAndRead. - The function has two
await
keywords: first, we wait for the function to write to the file test.txt - Second, we wait for the function to read the test.txt file we just wrote to.
- We store that in a variable and return it
- Because async functions return promises, I can use .then() after calling the writeAndRead() function.
Pretty sweet huh? We don't even need to specify a resolve() and reject() method anymore. Which brings me to the next point.
You are all the same errors to me <3
Let's assume a scenario where you have promises-based logic and non-promises-based logic ( synchronous and asynchronous ) in your code. You would probably handle errors this way:
const someComplicatedOperation = () => {
try {
// Blablabla do something
db.getUsers() //promise
.then( users => {
const data = JSON.parse( users ) // ===> What if this fail bro?
console.log(data)
})
.catch( err => {
console.log('You broke your promise!!!')
})
}
catch( err ){
console.log('I caught a error. But I only catch synchronous stuff here :(')
}
}
That's right. The try/catch won't catch the JSON.parse error because it is happening inside a promise. A rejected promise triggers the .catch() method, but NOT the other catch. That's annoying, we have to duplicate code for catching errors. Well, that time is now over with async/await!
const allErrorsAreDeclaredEqualInTheEyesOfAsyncAwait = async () => {
try {
const users = await db.getUsers
const data = JSON.parse( users )
console.log(data)
}
catch( err ){
console.log('All errors are welcomed here! From promises or not, this catch is your catch.')
}
}
Clean, concise and clean, while being concise. Good old try/catch can handle all the errors we can throw.
How high can you stack them errors?
As developers, if there is one thing we love, it is an infinite amount of functions in an error stack. It is probably not a huge deal, but more like a nice thing to know when you work with async/await. Check it out:
const stackingAllTheWayToTheSky = () => {
return usefulPromise()
.then(() => usefulPromise())
.then(() => usefulPromise())
.then(() => usefulPromise())
.then(() => usefulPromise())
.then(() => usefulPromise())
.then(() => {
throw new Error('I can see my house from here!!')
})
}
stackingAllTheWayToTheSky()
.then(() => {
console.log("You won't reach me.")
})
.catch(err => {
console.log(err) // FEEL THE PAIN!
})
//Error: I can see my house from here!!
// at stackingAllTheWayToTheSky.then.then.then.then.then.then (index.js:50:11)
Now with async/await:
const debuggingMadeFun = async () => {
await usefulPromise()
await usefulPromise()
await usefulPromise()
await usefulPromise()
await usefulPromise()
throw new Error('I will not stack.')
}
debuggingMadeFun()
.then(() => {
console.log('Not here')
})
.catch(err => {
console.log(err)
})
//Error: I will not stack
// at debuggingMadeFun (index.js:47:9)
Ain't that much much more cleaner and easy to read?
Values in the middle
You probably wrote some code where you executed one operation and used that to execute a second one. Finally, you need those two values for the third and final operation. So, you may write something like that:
const withPromises = () => {
return firstPromise()
.then( firstValue => {
return secondPromise( firstValue )
})
.then( secondValue => {
return thirdPromise( firstValue, secondValue )
})
}
// Or using Promise.all. It's a bit ugly, but the job is done
const withPromiseAll = () => {
return firstPromise()
.then(firstValue => {
return Promise.all([ firstValue, secondPromise(firstValue) ])
})
.then(([firstValue, secondValue]) => {
return thirdPromise(firstValue, secondValue)
})
}
Let's look at how much better it is with async/await:
const withAsyncAwait = async () => {
const firstValue = await firstPromise()
const secondValue = await secondPromise()
return thirdPromise( firstValue, secondValue )
}
Do I need to say more?
Conclusion
Well, async/await is a very cool way to write asynchronous code in Javascript. You can try it out in Node.js because it is natively supported since version 7.6. Have fun!!