Asynchronous codes are common in JS programming, like fetching data from an endpoint and reading dir/files. Often they require us to pass a callback function that will be executed when the action is completed.
The problem with callback async
The problem with callback async is that they can get messy.
If I want to read a file (using fs.readFile), I can do it like this:
fs.readFile('./file/location.md', 'utf-8', function(err, val){
if(err) throw new Error ("Something terrible happened")
console.log("Content: ", val)
})
console.log("Waiting...")
You'll notice "Waiting"
is displayed before "Content"
. This is because JS automatically moves all async functions at the back of the line (regardless how "fast" they execute).
Now this is a big deal if we need to use the result of that async function for our next action. If we need to use the result of our callback function, the following won't work:
let pathToNextLocation;
fs.readFile('./file/location1.md', 'utf-8', function(err, val){
if(err) throw new Error
pathToNextLocation = val;
})
console.log(pathToNextLocation);
We will need to do this instead:
let pathToNextLocation
fs.readFile('./file/location1.md', 'utf-8', function(err, val){
if(err) throw new Error
pathToNextLocation = val;
fs.readFile(pathToNextLocation, 'utf-8', function(err, val) {
// do stuff!
})
})
What if we need to execute four async functions in sequence? We would have to nest it four levels deep. This is one big spaghetti.
Better way to handle async: Promises
A better way to deal with async function is to use promises. Promises, like callbacks, are asynchronous. Unlike callbacks, they can be chained.
Promise takes 2 arguments and we need to resolve
it - think of it like Promise's own way to return value when it is done.
new Promise((resolve, reject) =>
resolve('Hello promise')
)
.then(value => console.log(value))
This then
chain is really awesome, because now we can do something like this:
asyncReadFile('./file/to/location1.md', 'utf-8')
.then(value => {
return anotherPromise
})
.then(value => {
return anotherPromise
})
.then(value => {
return yetAnotherPromise
})
// and so on
This looks MUCH better than callback spaghetti.
Putting the two together: replace all callbacks with promises
We learned two things:
- Too many callbacks leads to spaghetti code
- Chained promises are easy to read
However, callbacks functions are not the same thing as promises. fs.readFile
do not return promises. We can't just use then
chain on several fs.readFile
together.
"Hmm, I wonder if there is a way to convert them callbacks into promises so I can chain them and make them look pretty?" - me thinking
Absolutely!! Promisify does JUST that.
Promisify is part of util built into Node 8+. It accepts a function that accepts a callback function (wow, that's a mouthful). The resulting function is a function that returns a promise. Let's jump straight into it. It will make sense after we run it ourselves.
Let's create several files in a directory that contains the name of other files to read. Then we will read the first file - see if we can make it to the last file.
// file1.md
file2.md
// file2.md
file3.md
// file3.md
Finished!
// reader.js
const fs = require("fs");
const { promisify } = require("util");
const promiseReadFile = promisify(fs.readFile);
promiseReadFile("file1.md", "utf-8")
.then(content => {
const nextFileToRead = content.trim();
return promiseReadFile(nextFileToRead, "utf-8");
})
.then(content => {
const nextFileToRead = content.trim();
return promiseReadFile(nextFileToRead, "utf-8");
})
.then(content => {
console.log(content.trim());
});
Now let's node ./reader.js
and see what happens. You should see "Finished!"
printed.
Sweet! Now that is one spaghetti I don't mind eating.
Javascript has another way to handle promises: async/await.
To test your understanding, can you convert promisified code above from then
into async/await
?
Thanks for reading. Happy hackin'! Let me know if you have questions!