Flattening callback pyramids

#Flattening callback pyramids

You’ve seen how chaining promises lets you flatten out a callback pyramid. But to really get an intuition for this, there’s no substitute for practice. So let’s do an exercise!

In the editor below, I’ve started you off with a working app that asynchronously fetches and displays some random numbers (simulating cryptocurrency prices). There’s just a problem: the app is built with callbacks.

Your task is to flatten the callback pyramid as much as possible by replacing callbacks with promises.

The provided getTicker(), getPrice() and writeLog() functions all use the error-first callback convention. This means that:

  • If an error occurs, it’ll be passed to the callback as its first argument.
  • If the functions complete successfully, the first argument will be null.

In order to flatten the callback pyramid, you’ll need to create three functions that mirror the existing functions, but return a callback instead of accepting a promise. These functions can be implemented with new Promise() — if you get stuck, I explain the process in more detail in the error-first callbacks lesson.

///name:Flattening callback pyramids
///main.js
import { getTicker, getPrice, writeLog } from './api.js'

console.info('Getting latest price and writing to log...')

// Once you've got the refactored code working, change the error
// probability to 0.5 or 1 to test the error handling code.
setErrorProbability(0)

getTicker((err, ticker) => {
    if (err) {
        console.error(err)
        return
    }

    getPrice(ticker, (err, price) => {
        if (err) {
            console.error(err)
            return
        }

        writeLog(ticker, price, (err) => {
            if (err) {
                console.error(err)
                return
            }

            console.info("Done!")
        })
    })
})
///solution:main.js
import { getTicker, getPrice, writeLog } from './api.js'

console.info('Getting latest price and writing to log...')

// Once you've got the refactored code working, change the error
// probability to 0.5 or 1 to test the error handling code.
setErrorProbability(0)

function promisify(fn) {
  return (...args) => new Promise((resolve, reject) => {
    fn(...args, (err, value) => {
      if (err) {
        reject(err)
      }
      else {
        resolve(value)
      }
    })
  })
}

let getTickerPromise = promisify(getTicker)
let getPricePromise = promisify(getPrice)
let writeLogPromise = promisify(writeLog)

getTickerPromise()
  .then(ticker => {
     return getPricePromise(ticker).then(price => [ticker, price])
  })
  .then(([ticker, price]) => {
     return writeLogPromise(ticker, price)
  })
  .then(
    () => console.info("Done!"),
    err => console.error(err)
  )
///api.js
// Get the ticker whose price should be checked, with a delay
// to simulate network activity.
export const getTicker = (callback) =>
    simulate(callback, "getting ticker", () =>
        Math.random() > 0.5 ? 'DOGE' : 'BTC'
    )

// Get the price for a given ticker, with a delay to simulate
// network activity.
export const getPrice = (ticker, callback) =>
    simulate(callback, "getting price", () =>
        Math.random() * (ticker === 'DOGE' ? 10 : 1000)
    )

// Write the ticker and price to the console, with a delay to
// simulate writing the log to disk.
export const writeLog = (ticker, price, callback) =>
    simulate(callback, "writing log", () => {
        console.log(`Price for ${ticker}: ${price.toFixed(2)}`)
    })

// Return a promise that resolves in a random amount of time. 
// This can be used to simulate network or disk activity.
const simulate = (callback, message, getter) => {
    let delay = Math.random() * 1000
    setTimeout(() => {
        if (Math.random() < setErrorProbability.probability) {
            callback(`An error occurred while ${message}.`)
        }
        else {
            callback(null, getter())
        }
    }, delay)
}
///index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Untitled App</title>
  </head>
  <body>
    <div id="root"></div>
    <script>
        function setErrorProbability(probability) {
            setErrorProbability.probability = probability
        }
    </script>
    <script type="module" src="main.js"></script>
  </body>
</html>

One more hint: because writeLog() requires access to both ticker and price, you may find it a bit tricky to access both. If you get stuck here, you can click on this spoiler for some help:

Using what you know, you can nest calls to then() within one success handler to return an array with ticker and price. You can then keep the rest of your callback pyramid flat.

return getPricePromise(ticker).then(price => [ticker, price])

I’ll go over another way to handle this scenario in the next lesson.

Once you’ve given the exercise a solid try, compare your answer with mine by clicking the “Solution” button at the bottom of the editor. And once you’re ready, we’ll take a look at simpler approach to combining multiple promises.