Callbacks vs. Promises

stlnick

Nick

Posted on July 27, 2020

Callbacks vs. Promises

Basically whenever you need to get data from a server, do something with that data, maybe grab more data from another server you'll be using either callbacks or Promises.

It's a way to do something you need to do with that data once it comes back.

Callbacks

Callbacks are the parameter we use in many cases to provide functionality. A very common one is using a callback for addEventListener() when in web development.

document.querySelector('button').addEventListener('click', () => {
  // This is where your logic goes in the callback!
})
Enter fullscreen mode Exit fullscreen mode

The Bad

When we end up with a ton of if else statements nested, resulting in callback hell, we're again normally fetching data or manipulating data and need to ensure each step of the way an error hasn't occurred.

This example is from callbackhell.com and shows pretty well this issue.

This is going through a directory and attempting to resize files. It's not as important to understand the code here but simply the structure.

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})
Enter fullscreen mode Exit fullscreen mode

The Pyramid of Doom makes an appearance. On the left we see the indentation gradually getting further to the right and then closing with a bunch of } and }). Easy recipe to make simple errors based on one parenthesis or curly bracket.

Each step of the way we nest another if else and there is a total of 4 callback functions nested in there.

The Good

This is the only way as JavaScript developers we were able to do these asynchronous tasks prior to Promises.

It's a good thing to know of, be able to read and understand what's happening and why. There will be code out there you may work on for a long time most likely that has this structure.

Some people prefer it and that's totally fine! We all have our own preferences as devs and if the task is completed properly there's no problem truly.

Not to mention the example above isn't written as well as it could be. Again visit callbackhell.com as it has this example and goes through steps on how to improve it while still using callbacks.

Steps such as name your functions instead of using anonymous ones, try to keep it shallow when nesting and always handle each potential error.

Remember, only you can prevent callback hell and forest fires

--callbackhell.com

Promises

The Promise in JavaScript was made official in the release of ES6. Since then they've become a widely supported and accepted feature of JavaScript.

A Promise is essentially an object that represents some asynchronous task that will eventually succeed or fail. If it 'suceeds' we say it is fulfilled and we will use the .then() method to handle success.

If a Promise fails it is said to have been rejected and we will use the .catch() method to handle that failure and error.

The Bad

Promises are meant to be a asynchronous tasks. And since JavaScript lets us pretty much willy nilly write whatever we want we can end up with some bad code.

Craig Martin here on Dev.to has written a great article on the pitfalls of Promises in JavaScript.

Definitely read that article for a much better explanation on this than I could provide you. I'll quote him directly from the Intro of his article there with his three biggest issues with Promises:

  • Promises have an API which encourages casually dangerous code
  • Promises lack a convenient API to safely work with data.
  • Promises co-mingle rejected promises and unintended runtime exceptions

We always have tradeoffs in writing code and a Promise is no different.

The Good

Using a Promise can result in some more succinct, clean code than nesting callbacks. Take that with a grain of salt because again we've all got our preferences and that may not be true for all!

I also believe it's easier to debug and separate concerns as we can modularize our code a little more when converting to Promises.

I took just the first part of the above callback example and converted it to return a Promise:

const readDirPromise = (source) => {
  // Return a new Promise
  return new Promise((resolve, reject) => {
    // Do the actual file system read
    fs.readdir(source, (err, files) => {
      // If there's a problem reject with error thrown
      if (err) reject(err)
      // Else we succeed and resolve with the files
      resolve(files)
    }
  })
}

readDirPromise(source)
  .then(data => {/* Do something with data */})
  .catch(err => {/* Handle error */}
Enter fullscreen mode Exit fullscreen mode

This way we don't have to nest a whole lot. We place this piece of logic in a function that will either resolve and move on or reject and show us the error.

If we continued to convert that whole callback example we would simply take each part with a specific task and place that inside its own function like readDirPromise and call those in the then() statements.

readDirPromise(source)
  .then(data => {/* secondStep() */})
  .then(data => {/* thirdStep() */})
  .then(data => {/* fourthStep() */})
  .catch(err => {/* Handle error */}
Enter fullscreen mode Exit fullscreen mode

Each time we chain then()s we're expecting the last step to resolve or succeed. If at any point there is an error our single catch() statement will handle it instead of having an if (err) a whole bunch. Then we can place the code for handling each type of error in just the catch().

Final Thoughts

I think Promises are great and want to do more with them. That said I worked with callbacks first and certainly have an appreciation for them. Especially in simpler tasks it might be easier just to use callbacks. Once more they're present in a lot of code because that was the way to achieve our asynchronous goals in the dark pre-ES6 days.

Having a solid understanding of the foundational pieces of JavaScript, in my mind, helps us understand and appreciate all the awesome, new features we've gotten in just the last handful of years.

πŸ’– πŸ’ͺ πŸ™… 🚩
stlnick
Nick

Posted on July 27, 2020

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

Sign up to receive the latest update from our blog.

Related