Callbacks vs. Promises
Nick
Posted on July 27, 2020
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!
})
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))
}
})
})
}
})
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 Promise
s.
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
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
Promise
s 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 Promise
s 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 */}
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 */}
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 Promise
s 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.
Posted on July 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.