Dan D Kim

Let's share stories

The Ultimate Javscript Promises Ramp-Up

2019-06-10 Dan D. Kimjavascript


I recently joined a new team and had to ramp-up on my Javascript knowledge. Promises were a standard concept that I needed to learn about. After a bit of ramping-up, I have written this tutorial in hopes to share what I have learned, all in one curated post.

This tutorial is geared towards those who have done Javascript before, and want to learn the powerful and popular concept - Promises. Maybe you played with it before. Maybe you are new to it. In either case, the goal of this post is to teach you how useful it can be for you.

If you are looking to ramp-up on your knowledge with Javascript Promises, you are at the right place!!

The code here follows StandardJS Rules.

Topics

Promise states

Here are the 4 states of a Promise that you need to know:

"Promise States"

Pending - The final value of the promise is not available yet.

Settled - The promise has either been fulfilled or rejected.

Fulfilled - Success! This state is also known as resolved. Final value becomes available.

Rejected - An error has prevented the final value from being determined.

Promise.prototype.then()

.then(onFulfilled[, onRejected]) will take in the final value the promise returns.

Simple example

Here, we will make a simple fetch call to the Reddit frontpage.

const redditPromise = fetch('https://www.reddit.com/.json')

redditPromise.then(value => {
  console.log(`Success! We have the result: ${value.status}`) // Success! We have the result: 200
})

Return undefined

Here, we see that undefined values can be propagated.

redditPromise
  .then(response => response.undefined_property)
  .then(response => console.log(response)) // undefined

Chained promises with .then()

Here is a simple example that chains two .then() calls. The return value of the first call propagates to the next.

redditPromise
  .then(value => value.json())
  .then(value => console.log(value.data.children.length)) // 25

Promise.prototype.catch()

Okay so we know how to use the then() for fulfilled values, but what if an error is thrown? We could handle the error within the then() block as shown:

const redditPromise = fetch('https://www.reAAAAAddit.com/.json') // Invalid endpoint

redditPromise.then(
  response => {
    // Fulfilled
    console.log('Success') // Not printed.
  },

  error => {
    // Rejected
    console.error(error) // Printed.
  })

That’s great, but what if an error is thrown from within the then() scope?

const redditPromise = fetch('https://www.reddit.com/.json') // Valid endpoint

redditPromise.then(
  response => {
    console.log('Success') // Printed
    throw Error() // Unhandled Promise Rejection
  },

  error => {
    console.error(error) // Not printed
  })

The error won’t be caught and you will have a crash. :(

Error handling with .then() vs .catch()

Should you catch errors in the .then() or in .catch()? What’s the difference?

To understand this, let’s look at the following error flow.

"Promise error handling with just .then()"

.then() is not responsible for errors that happen within its scope. It will just pass it on down the chain.

Let’s look at the difference with .catch().

"Promise error handling with .catch()"

Here, we see that errors thrown within the .then() scope are caught!

So, here is the updated code.

redditPromise.then(
  response => {
    console.log('Success')
    throw Error()
  })
  .catch(error => {
    console.error(error) // Error is caught!
  })

Promise.prototype.finally()

If you are familiar with try-catch-finally, then this one should be straight forward.

Here’s a question though, what’s the behavioral difference between using the following 2 patterns?

  1. try - catch - then
  2. try - catch - finally

Answer: if an error is thrown from the catch, finally in scenario 2 will run anyways, but then in scenario 1 won’t unless it covers the error.

Scenario 1

const redditPromise = someBadFetch() // response.ok = false

redditPromise.then(
  response => {
    if (!response.ok) {
      throw Error('unsuccessful response') // Throws
    }
    return response.json()
  })
  .catch(error => {
    handleError(error)
    throw Error() // Uh oh
  })
  .then(
    () => {
      cleanUp() // Not called
    },
    ()) => {
      cleanUp() // Called
    }
  )

Scenario 2

const redditPromise = someBadFetch() // response.ok = false

redditPromise.then(
  response => {
    if (!response.ok) {
      throw Error('unsuccessful response') // Throws
    }
    return response.json()
  })
  .catch(error => {
    handleError(error) // Called
  })
  .finally(() => {
    cleanUp() // Called
  })

.finally() will always be called, regardless of whether or not .catch() throws an error.

Promise.reject()

The opposite of .resolve(). You can create a Rejected Promise with .reject().

Promise.reject('nope')
  .then(response => {
    console.log('then') // Not printed
  })
  .catch(error => {
    console.log(error) // Prints 'nope'
  })

Normally, you would pass in an error to the reject, not a string.

Promise.reject(Error('nope'))
  .then(response => {
    ...
  })
  .catch(error => {
    ...
  })

Nesting promises will still return the final error.

Promise.reject(Error('nope'))
  .then(response => {
    ...
  })
  .catch(error => {
    return Promise.reject(Error('naw'))
  })
  .catch(error => {
    handleError(error) // Error('naw')
  })

Promise.resolve()

.resolve() will return a promise that immedately fulfills with a value.

const resolvePromise = Promise.resolve(2000) // Promise that is immediatedly fulfilled with 2000.

resolvePromise.then(value => console.log(value)) // Prints 2000

Promise.resolve(resolvePromise) === resolvePromise // true

You might think you understand .resolve(), but pay careful attention! There are some counter-intuitive things here.

Resolving a rejected promise

It doesn’t always return a fulfilled promise. Consider the following example where a promise tries to resolve a rejected promise.

const rejectedPromise = Promise.reject(Error('Bleh bleh'))

Promise.resolve(rejectedPromise)
  .then(() => console.log('Fulfilled')) // Not called
  .catch(() => console.error('Rejected')) // Called

Makes sense, right? But then consider the following example where the promise resolves an error.

Promise.resolve(Error)
  .then(value => console.log(value)) // Prints out the error value
  .catch(error => console.error(error)) // Not called

It’s fulfilled. emoji-confused This is because there is no special treatment for the Error that is resolved. A Promise that is rejected will propagate towards the next error handler.

Convert a Thenable method into a native Promise with .resolve()

Let’s say you have a thenable method, like the following jQuery:

$.getJSON('https://www.reddit.com/.json')

// Result
// {readyState: 1, getResponseHeader: ƒ, getAllResponseHeaders: ƒ, setRequestHeader: ƒ, overrideMimeType: ƒ, …}

If you try to use the .finally(), it won’t work. :(

$.getJSON('https://www.reddit.com/.json')
  .then(value => ... )
  .catch(err => ... )
  .finally(() => ...)

// Uncaught TypeError: $.getJSON(...).then(...).catch is not a function

That’s because Promises came out a long time after jQuery, so you can’t really expect jQuery thenables to work like promises.

But, you can convert them into a native Promise with .resolve(), like in the following:

Promise.resolve($.getJSON('https://www.reddit.com/.json') )
  .then(result => doWork(result))
  .catch(err => handleError(err))
  .finally(() => cleanUp())

Create a new Promise

Creating a new Promise can be a useful way of encapsulating the workflow within promises.

Simple example

const promise = new Promise((resolve, reject) => {
  // Do some stuff.
  if (ok) {
    resolve(result)
  } else {
    reject(error)
  }
})

promise.then(
  result => {
    // Fulfilled
  },
  error => {
    // Rejected
  }
)

Scenario: Throwing an unexpected error

Can you guess what would happen in the following code?

const someBuggyPromise = new Promise(resolve => {
  throw Error('Whoa')
  resolve('Hello Promises')
})

Even if the promise doesn’t .reject() anything, if an Error is thrown, it will be rejected.

const someBuggyPromise = new Promise(resolve => {
  throw Error('Whoa')
  resolve('Hello Promises')
})

someBuggyPromise.then(
  result => {
    // Not called
  },
  error => {
    // Called
  }
)

Promise.race()

I haven’t had to use this one much, but I can see it being useful for setting timeouts on certain calls.

You can use .race() to find the first promise that settles.

const promiseA = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, '1')
  })
const promiseB = new Promise((resolve, reject) => {
  setTimeout(resolve, 10000, '2')
})

const fasterPromise = Promise.race([ promiseA, promiseB ])

fasterPromise.then(value => {
  console.log(value) // Prints 1
})

Don’t forget to clear timers.

How long do you think the above code will take? promiseA resolves after 1 second, so roughly around that, right?

Let’s run the following command to find out.

# Bash script
time node race.js

# Result
# real    0m10.319s
# user    0m0.015s
# sys     0m0.015s

Uh-oh. Our timeout for promiseB with 10 seconds prevents Node.js from terminating the process.

For the sake of this example, let’s say we kept a timer Id for each timer.

let timerA, timerB

const promiseA = new Promise((resolve, reject) => {
  timerA = setTimeout(resolve, 1000, '1')
  })
const promiseB = new Promise((resolve, reject) => {
  timerB = setTimeout(resolve, 10000, '2')
})

const fasterPromise = Promise.race([ promiseA, promiseB ])

fasterPromise.then(value => {
  console.log(value) // Prints 1
})
  .finally(()=> {
    clearTimeout(timerA)
    clearTimeout(timerB)
  })

Let’s run the script again.

time node race.js

# Result
# real    0m1.333s
# user    0m0.000s
# sys     0m0.030s

BAM. emoji-sunglasses emoji-thumbsup

Promise.all()

.all() takes in an iterable of promises, and returns a single promise that resolves when all promises that were passed in have resolved.

If one of the promises are rejected, then it will reject with the same error.

This is useful when you want to run async calls in parallel.

For the following example, we will try to get the latest price changes for Bitcoin and Ethereum. I have found them quite interesting, and want to know how the prices have changed in the past 24 hours. Let’s do that with promises.

function fetchCoinStats (coinId) {
    return fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)
      .then(response => {
        return response.ok
          ? response.json()
          : Promise.reject(Error('Error with request'))
      })
  }

const bitcoinPromise = fetchCoinStats(90) // Bitcoin
const ethereumPromise = fetchCoinStats(80) // Ethereum

bitcoinPromise
    .then(bitcoinStats => {
        ethereumPromise.then(ethStats => {
          const message = `Bitcoin price has changed ${bitcoinStats[0].percent_change_24h}% in the last day, while Ethereum has changed ${ethStats[0].percent_change_24h}%.`
          displayOnUI(message)
        })
    })
    .finally(() => {
      cleanUpUI()
    })

Depending on your network, you might get the wanted outcome where displayOnUI() finishes first, then cleanUpUI() is called after. But this may not always be the case. cleanUpUI() could be called before displayOnUI finishes.

This is because ethereumPromise is a dangling promise. It’s not returned, so it never gets passed onto the promise chain.

Let’s change that.

bitcoinPromise
    .then(bitcoinStats => {
        return ethereumPromise.then(ethStats => {
          const message = `Bitcoin price has changed ${bitcoinStats[0].percent_change_24h}% in the last day, while Ethereum has changed ${ethStats[0].percent_change_24h}%.`
          displayOnUI(message)
        })
    })
    .finally(() => {
      cleanUpUI()
    })

Now, cleanUpUI() will always be called after both promises are finished.

But, it’s still not ideal. Right now, our promises are working like they are in a baton passing race. "Baton pass race" The call for ethereum starts after the call for bitcoin is fulfilled.

But they are both asynchronous calls, so it doesn’t have to be that way.Let’s make them happen in parallel with .all().

const promise = Promise.all([
  bitcoinPromise,
  ethereumPromise
])

promise.then(results => {
  const bitcoinStats = results[0]
  const ethStats = results[1]
  const message = `Bitcoin price has changed ${bitcoinStats[0].percent_change_24h}% in the last day, while Ethereum has changed ${ethStats[0].percent_change_24h}%.`
  displayUI(message)
})
  .finally(() => {
    cleanUpUI()
  })

Try running the following code in your browser console.

function fetchCoinStats (coinId) {
  return fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)
    .then(response => {
      return response.ok
        ? response.json()
        : Promise.reject(Error('Error with request'))
    })
}

const bitcoinPromise = fetchCoinStats(90) // Bitcoin
const ethereumPromise = fetchCoinStats(80) // Ethereum

const promise = Promise.all([
  bitcoinPromise,
  ethereumPromise
])

promise.then(results => {
  const bitcoinStats = results[0]
  const ethStats = results[1]
  const message = `Bitcoin price has changed ${bitcoinStats[0].percent_change_24h}% in the last day, while Ethereum has changed ${ethStats[0].percent_change_24h}%.`
  console.log(message)
})

Async Await

I personally find that code with async / await are easier to read than code with Promises. They are very useful, and they are still technically Promises. An easy way to go about it is to see async / await as Promise wrappers.

Let’s go back to the previous example with Bitcoin.

Say I had the following code to get the latest Bitcoin price.

function getBitcoinStats() {
  return fetchCoinStats(90).then(stats => {
    console.log(stats[0].price_usd)
  })
}

It returns a promise that will print the price of Bitcoin when fulfilled.

Let’s change it to an async await operation.

async function getBitcoinStats() {
  const stats = await fetchCoinStats(90)
  console.log(stats[0].price_usd)
}

getBitcoinStats() // Will fetch and print Bitcoin price

Explanation

  • async is marking the function as asynchronous. This is done whenever there is an await call within the function.
  • await is making the program wait for the function fetchCoinStats() to be settled (remember the states?). It then returns the fulfilled value.

Question: what if an error is thrown from fetchCoinStats()? Answer: then the method getBitcoinStats() will be equivalent to a rejected Promise that returns the error from fetchCoinStats().

async function getBitcoinStats() {
  const stats = await fetchCoinStats(90) // Let's say this throws and error
  console.log(stats[0].price_usd)
}

getBitcoinStats().catch(error => console.log(error)) // Catch the error

It works, but the code doesn’t look so good anymore. I need to look at multiple places to understand the workflow.

Instead, we can have try - catch - finally inside `getBitcoinStats()

async function getBitcoinStats() {
  try {
    const stats = await fetchCoinStats(90)
    console.log(stats[0].price_usd)
  } catch (error) {
    handleError(error)
  } finally {
    cleanUp()
  }
}

getBitcoinStats()

The code looks a lot more readable, and the workflow is all in one place! Great!

here’s another example. Let’s convert our fetchCoinStats() method into async await.

Original

function fetchCoinStats (coinId) {
  return fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)
    .then(response => {
      return response.ok
        ? response.json()
        : Promise.reject(Error('Error with request'))
    })
}

Async Await

async function fetchCoinStats (coinId) {
  const response = await fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)

  if (response.ok) {
    return repsonse.json()
  } else {
    return Promise.reject(Error('Error with request'))
  }
}

Finally, let’s combine with Promise.all() to make requests for other coins too.

You can try running the following code in your browser.

async function fetchCoinStats (coinId) {
  const response = await fetch(`https://api.coinlore.com/api/ticker/?id=${coinId}`)

  if (response.ok) {
    return response.json()
  } else {
    return Promise.reject(Error('Error with request'))
  }
}

async function getCryptoCoinStats() {
  try {
    const [bitcoin, ethereum, bitcoinCash] = await Promise.all([
      fetchCoinStats(90), // Bitcoin
      fetchCoinStats(80), // Ethereum
      fetchCoinStats(2321) // Bitcoin cash
    ])
    const message = `Bitcoin price has changed ${bitcoin[0].percent_change_24h}% in the last day, Ethereum has changed ${ethereum[0].percent_change_24h}%, and Bitcoin cash has changed ${bitcoinCash[0].percent_change_24h}%.`
    console.log(message)
  } catch (error) {
    console.log(error)
  } finally {
    console.log('done')
  }
}

getCryptoCoinStats()

That’s all!

Hope you learned something. Feel free to send me feedback.

Cheers and Happy Promises :)