Skip to content

Instantly share code, notes, and snippets.

@soeirosantos
Created October 17, 2017 13:05
Show Gist options
  • Save soeirosantos/29e8ae74fd6b6eeed0fcea4d4d40ba98 to your computer and use it in GitHub Desktop.
Save soeirosantos/29e8ae74fd6b6eeed0fcea4d4d40ba98 to your computer and use it in GitHub Desktop.
Async functions in a usage context

Async functions in a usage context

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment