Async Functions from Start to Finish

jacobmparis

Jacob Paris

Posted on March 29, 2020

Async Functions from Start to Finish

Functions

A function is a block of code that can be called and executed at will

function setTitle() {
  document.title = 'Async, Await, and Promises'
}
Enter fullscreen mode Exit fullscreen mode

This gives us a function named setTitle. To execute it, call it by name with parentheses after, like setTitle()

Before: Alt Text

After: Alt Text

Arguments

Functions can also have arguments, which are variables you pass into a function when you call it.

function setTitle(title) {
  document.title = title
}

setTitle('Async, Await, and Promises')
Enter fullscreen mode Exit fullscreen mode

This makes functions much more reusable, since you can call it with any value you want

setTitle("Who me?")
setTitle("Yes you.")
setTitle("Couldn't be")
setTitle("Then who?")
Enter fullscreen mode Exit fullscreen mode

Callbacks

When you call a function, sometimes it can call back to another function

The setTimeout function accepts two arguments: a callback function, which it executes when it's finished waiting, and a delay, which is the number of milliseconds to wait

function setTimeout(callback, delay)
Enter fullscreen mode Exit fullscreen mode

We can use this to call our original setTitle function automatically after one second.

function setTitle() {
  document.title = 'Async, Await, and Promises'
}

setTimeout(setTitle, 1000)
Enter fullscreen mode Exit fullscreen mode

This works since we're setting the title explicitly, but if we try to pass it in as an argument it just clears the title, shown below

function setTitle(title) {
  document.title = title
}

setTimeout(setTitle, 1000)
Enter fullscreen mode Exit fullscreen mode

What happened? Since the callback (setTitle) is executed by the function (setTimeout) we don't have control over what arguments setTitle is called with.

So instead of passing setTitle as our callback, we can make our callback a wrapper function instead

// Pattern 1: Named Function
function wrappedSetTitle() {
  setTitle('Async, Await, and Promises')
}
setTimeout(wrappedSetTitle, 1000)
Enter fullscreen mode Exit fullscreen mode
// Pattern 2: Anonymous Function
setTimeout(function () {
  setTitle('Async, Await, and Promises')
}, 1000)
Enter fullscreen mode Exit fullscreen mode
// Pattern 3: Arrow Function
setTimeout(() => {
  setTitle('Async, Await, and Promises')
}, 1000)
Enter fullscreen mode Exit fullscreen mode
// Pattern 4: Inline Arrow function
setTimeout(() => setTitle('Async, Await, and Promises'), 1000)
Enter fullscreen mode Exit fullscreen mode

Now setTimeout will wait until 1000 milliseconds have passed, then invoke our wrapper function which calls setTitle with a title of our choice

Promises

We saw how to create functions and use them as callbacks

A Promise is a class that executes a callback and allows you to trigger other promises when the callback completes or fails.

function promiseTimeout(delay) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(), delay)
  }).then(() => {
    setTitle('Async, Await, and Promises')
  })
}

promiseTimeout(1000)
Enter fullscreen mode Exit fullscreen mode

There's a lot going on here, so we'll break it down from the inside out

First, setTimeout waits until the delay is up, then triggers the callback by running the Promise's resolve() function

The callback to a Promise is defined by chaining a method called .then(callback)

Right now it seems like it's just a more complicated way of writing callbacks, but the advantage comes in when you want to refactor

function promiseTimeout(delay) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(), delay)
  })
}

promiseTimeout(1000)
  .then(() => setTitle('Async, Await, and Promises'))
Enter fullscreen mode Exit fullscreen mode

The .then() method always returns a promise. If you try to return a regular value, it will return a promise that resolves instantly to that value

Since it returns a promise, you can chain .then() onto the result indefinitely

So either of these patterns are valid

promiseTimeout(1000)
  .then(() => {
    setTitle('Async, Await, and Promises')
    setTitle('Async, Await, and Promises')
    setTitle('Async, Await, and Promises')
  })
Enter fullscreen mode Exit fullscreen mode
promiseTimeout(1000)
  .then(() => setTitle('Async, Await, and Promises'))
  .then(() => setTitle('Async, Await, and Promises'))
  .then(() => setTitle('Async, Await, and Promises'))
Enter fullscreen mode Exit fullscreen mode

If the callback passed to .then() is a promise, it will wait for the promise to resolve before executing the next .then()

promiseTimeout(1000)
  .then(() => setTitle('One second'))
  .then(() => promiseTimeout(5000)
  .then(() => setTitle('Six total seconds'))
Enter fullscreen mode Exit fullscreen mode

Constructor

One way to create a Promise is through the constructor. This is most useful when you're wrapping a function that uses non-promise callbacks.

const promise = new Promise((resolve, reject) => {
  resolve(data) // Trigger .then(callback(data))
  reject(error) // Trigger .catch(callback(error))
})
Enter fullscreen mode Exit fullscreen mode

To use a real-world example, Node.js has a method for loading files called readFileAsync that looks like this

fs.readFileAsync('image.png', (error, data) => { })
Enter fullscreen mode Exit fullscreen mode

If we want to turn that into a promise, we're going to have to wrap it in one.

function getImage(index) {
  return new Promise((resolve, reject) => {
    fs.readFileAsync('image.png', (error, data) => {
      if (error) {
        reject(error)
      } else {
        resolve(data)
      }
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

Tip: Node.js includes a promisify method that transforms a function that uses traditional callbacks to one that returns a promise. The implementation is exactly like the example above.

Class Method

Another way to create a promise is use the static class methods

Promise.resolve('value') will return a resolved promise. It will immediately begin to execute the next .then() method it has, if any.

Promise.reject('error') will return a rejected promise. It will immediately begin to execute the next .catch() method it has, if any.

function getProducts() {
  if(!isCacheExpired) {
    return Promise.resolve(getProductsFromCache())
  }

  // The built-in method fetch() returns a promise
  return fetch('api/products') 
    .then(response => response.json())
    .then(products => {
      saveProductsToCache(products)

      return products
    })
}
Enter fullscreen mode Exit fullscreen mode

Imagine you're trying to download a list of products from an API. Since it doesn't change very often, and API requests can be expensive, you might want to only make API requests if the list you already have is more than a few minutes old.

First we check if the cache has expired, and if not we return a promise resolving to the products we've already saved to it.

Otherwise the products are out of date, so we return a promise that fetches them from the API, saves them to the cache, and resolves to them.

Catch

While .then() triggers when a previous promise resolves, .catch() triggers when a previous promise either rejects or throws an error.

If either of those happen, it will skip every .then() and execute the nearest .catch()

fetch('api/products') 
  .then(response => response.json())
  .then(products => {
    saveProductsToCache(products)

    return products
  })
  .catch(console.error)
Enter fullscreen mode Exit fullscreen mode

If .catch() returns anything or throws another error, it will continue down the chain just like before

Async Functions

To make writing promises easier, ES7 brought us the async keyword for declaring functions

A function declared with the async keyword always returns a promise. The return value is wrapped in a promise if it isn't already one, and any errors thrown within the function will return a rejected promise.

Usage

This is how to use it in a function

async function getProducts() { }

const getProducts = async function() => { }

const getProducts = async () => { }
Enter fullscreen mode Exit fullscreen mode

And in a method:

const products = {
  async get() { }
}
Enter fullscreen mode Exit fullscreen mode

Return

Whenever an async function returns, it ensures its return value is wrapped in a promise.

async function getProducts() {
  return [
    { id: 1, code: 'TOOL', name: 'Shiny Hammer' },
    { id: 2, code: 'TOOL', name: 'Metal Corkscrew' },
    { id: 3, code: 'TOOL', name: 'Rusty Screwdriver' },
    { id: 1, code: 'FOOD', name: 'Creamy Eggs' },
    { id: 2, code: 'FOOD', name: 'Salty Ham' }
  ]
}

getProducts()
  .then(products => {
    console.log(products)
    // Array (5) [ {…}, {…}, {…}, {…}, {…} ]
  })
Enter fullscreen mode Exit fullscreen mode

Throw

If an async function throws an error, it returns a rejected promise instead. This can be caught with the promise.catch() method instead of wrapping the function in try/catch statements

async function failInstantly() {
  throw new Error('oh no')
}

failInstantly()
  .catch(error => {
    console.log(error.message)
    // 'oh no'
  })
Enter fullscreen mode Exit fullscreen mode

In a regular function, you need to catch errors using the classic try/catch statement syntax

function failInstantly() {
  throw new Error('oh no')
}

try {
  failInstantly()
} catch (error) {
  console.log(error.message)
  // 'oh no'
}
Enter fullscreen mode Exit fullscreen mode

Await

The other difference between regular functions and async functions is that async functions allow the use of the await keyword inside.

Await works like the .then() method, but instead of being a chained callback, it pulls the value out of the promise entirely.

Consider the previous example

getProducts()
  .then(products => {
    console.log(products)
    // Array (5) [ {…}, {…}, {…}, {…}, {…} ]
  })
Enter fullscreen mode Exit fullscreen mode

And the same with await

const products = await getProducts()

console.log(products)
// Array (5) [ {…}, {…}, {…}, {…}, {…} ]
Enter fullscreen mode Exit fullscreen mode

It's important to remember that since await can only be used inside async functions (which always return a promise) you can't use this to pull asynchronous data out into synchronous code. In order to use await on a promise you must be inside another promise.

💖 💪 🙅 🚩
jacobmparis
Jacob Paris

Posted on March 29, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related