The task we want to accomplish in this example is, given a URL and a file system path, download the document content and save it in the file system.
Let's begin our example by using the modules request
and fs
to perform the task directly:
const request = require('request')
const fs = require('fs')
const URL = 'https://news.ycombinator.com/bigrss'
const FILE_PATH = './news/hn.xml'
request.get(URL, (err, res, body) => {
if (err) throw err
fs.writeFile(FILE_PATH, body, (err) => {
if (err) throw err
console.log('Saved:', FILE_PATH)
})
})
Ok, a simple task, although the things can get a little bit tricky here. Note that we have two levels of nested callbacks and the modules are really mixed. In this simple example it doesn't seem like a big deal, but as I'd use this piece of code abroad my application, some maintenance issues could emerge.
In order to make this code easier to maintain, we want to address two points: decouple our application from the underlying modules and avoid a possible callback hell. Let's re-write this code and in a step-by-step approach and improve it.
First, let's abstract the underlying modules and provide a better interface for our domain:
[...]
function downloadContent(url, onSuccess, onError) {
request.get(URL, (err, res, content) => {
if (err) return onError(err)
onSuccess(content)
})
}
function saveFile(filePath, content, onSuccess, onError) {
fs.writeFile(filePath, content, (err) => {
if (err) return onError(err)
onSuccess(filePath)
})
}
const errorHandler = (error) => console.error('Error occurred: %s', error)
downloadContent(URL, (body) => {
saveFile(FILE_PATH, body, (filePath) => {
console.log('File saved: ' + filePath)
}, errorHandler)
}, errorHandler)
Ok, I'd say it's a little bit better. We are no longer directly dependent on the request
and fs
modules, changes and improvements can be easily applied and propagated through the application.
Now, let's avoid the use of nested function calls using Promises
:
[...]
function downloadContent(url) {
return new Promise((onSuccess, onError) => {
request.get(URL, (err, res, content) => {
if (err) return onError(err)
onSuccess(content)
})
})
}
function saveFile(filePath, content) {
return new Promise((onSuccess, onError) => {
fs.writeFile(filePath, content, (err) => {
if (err) return onError(err)
onSuccess(filePath)
})
})
}
const errorHandler = (error) => console.error('Error occurred: %s', error)
function downloadTo(url, filePath) {
downloadContent(URL)
.then((content) => saveFile(FILE_PATH, content))
.then((filePath) => console.log('File saved: ', filePath))
.catch(errorHandler)
}
downloadTo(URL, FILE_PATH)
Note that by simply wrapping the body of our functions downloadContent
and saveFile
with a Promise object was enough to improve the usage and avoid the nested callbacks. We also introduced the downloadTo
function that encapsulate the details of the download-and-save feature.
OBS: To keep this step simple we changed the name of the well-known promise parameters resolve
and reject
by onSuccess
and onError
. I don't recommend to do it in your code.
Finally, to finish our example we'll use the async/await
syntax that simplify dramatically the reading and overall understanding of the code:
[...]
async function downloadTo(url, filePath) {
try {
const content = await downloadContent(url)
const savedAt = await saveFile(filePath, content)
console.log('File saved: ', savedAt)
} catch (e) {
errorHandler(e)
}
}
downloadTo(URL, FILE_PATH)
In summary, we can use promises in a like-synchronous approach using the await
notation in an async
declared function. In this case, we handle errors using a try/catch block.
"The purpose of async/await functions is to simplify the behavior of using promises synchronously and to perform some behavior on a group of Promises. Just as Promises are similar to structured callbacks, async/await is similar to combining generators and promises."
For more details and a full explanation of async functions, please, check the JavaScript Async Reference.
The purpose of this example was to show how we can evolve from a naive implementation to a well defined and easy to maintain interface using the async/await
functions.
I hope you have enjoyed this text.