html effects

Callbacks vs. Promises in JavaScript

If you’re new to JavaScript and have a hard time trying to understand how promises work, hopefully this article will assist you in understanding them more clearly.

With that said, this article is aimed at those who are a little unsure about promises.

This post won’t be going over executing promises using async/await (although they’re the same thing functionality-wise — only that async/await is more syntactic sugar for most situations).


The What

Promises were actually out for awhile even before they were native to JavaScript. For example, two libraries that implemented this pattern before promises became native is Q and when.

So what are promises? Promises are JavaScript objects that represent an eventual completion or failure of an asynchronous operation. You can achieve results from performing asynchronous operations using the callback approach or by using promises. But there are some minor differences between the two.


Key Difference Between Callbacks and Promises

A key difference between the two is when using the callback approach, we’d normally just pass a callback into a function that would then get called upon completion in order to get the result of something. In promises, however, you attach callbacks on the returned promise object.

Callbacks:

Promises:


The Promise Object

It’s good we just mentioned promise objects because they’re the core that make up promises in JavaScript.

So the question is why do we need promises in JavaScript?

Well, to better answer this question, we’d have to ask why using the callback approach just wasn’t enough for the majority of JavaScript developers out there.


Callback Hell

One common issue for using the callback approach is that when we end up having to perform multiple asynchronous operations at a time, we can easily end up with something that is known as callback hell, which can become a nightmare as it leads to unmanageable and hard-to-read code. In other words, it’s every developer’s worst nightmare.

Here’s an example of that:

You can visually see in the code snippet that there’s some awkward shape building up. Just from three asynchronous API calls, callback hell has begun sinking opposite of the usual top-to-bottom direction.

With promises, it no longer becomes an issue as we can keep the code at the root of the first handler by chaining the .then methods:

In the callback code snippet, if we were nested just a few levels deeper, things would start to get ugly and hard to manage.


Problems Occurring From Callback Hell

Just by looking at our previous code snippet representing this callback hell, we can come up with a list of dangerous issues that were emerging from it that serve as enough evidence to say promises were a good addition to the language:

It was getting harder to read

The code was beginning to move in two directions (top to bottom, then left to right)

It was getting harder to manage

If we look closely at the examples, we’ll notice most of these issues were solved by being able to chain promises with .then, which we’ll talk about next.


Promise Chaining

Promise chaining becomes absolutely useful when we need to execute a chain of asynchronous tasks. Each task that’s being chained can only start as soon as the previous task has completed, controlled by the .thens of the chain.

Those .then blocks are internally set up so they allow the callback functions to return a promise, which are then subsequently applied to each .then in the chain.

Anything you return from .then ends up becoming a resolved promise, in addition to a rejected promise coming from .catch blocks.

Here’s a short and quick example of that:


Promise Methods

The promise constructor in JavaScript defines several static methods that can be used to retrieve one or more results from promises:

Promise.all

When you want to accumulate a batch of asynchronous operations and eventually receive each of their values as an array, one of the promise methods that satisfy this goal is Promise.all.

Promise.all gathers the result of the operations when all operations ended up successful. This is similar to Promise.allSettled — only here the promise rejects with an error if at least one of these operations ends up failing. This eventually ends up in the .catch block of the promise chain.

Promise rejections can occur at any point — from the start of its operation to the time that it finishes. If a rejection occurs before all of the results complete, then what happens is those that didn’t get to finish will end up aborted and will end up never finishing. In other words, its one of those all or nothing deals.

Here’s a simple code example where the Promise.all method consumes getFrogs and getLizards, which are promises. It retrieves the results as an array inside the .then handler before storing them into the local storage:

Promise.race

This method returns a promise that either fulfills or rejects whenever one of the promises in an iterable resolves or rejects with either the value or the reason from that promise.

Here is a simple example between promise1 and promise2 and the Promise.racemethod in effect:

This will yield this result:

The returned value ended up being the promise rejection since the other promise was delayed behind by 200 milliseconds.

Promise.allSettled

The Promise.allSettled method ultimately somewhat resembles Promise.all in sharing a similar goal except instead of immediately rejecting into an error when one of the promises fails, Promise.allSettled will return a promise that eventually always resolves after all of the given promises have either resolved or rejected, accumulating the results into an array where each item represents the result of their promise operation.

What this means is that you’ll always end up with an array data type. Here’s an example of this in action:

Promise.any

Promise.any is a proposal adding onto the Promise constructor, which is currently on stage 3 of the TC39 process.

What Promise.any is proposed to do is accept an iterable of promises and attempt to return a promise that’s fulfilled from the first given promise that’s fulfilled or rejected with an AggregateError holding the rejection reasons if all of the given promises are rejected (source).

This means if there was an operation that consumed 15 promises and 14 of them failedwhile one was resolved, then the result of Promise.any becomes the value of the promise that resolved:

Read more about it here.


Success/Error Handling Gotcha

It’s good to know handling successful or failed promise operations can be done using these variations:

Variation 1:

add(5, 5).then(
function success(result) {
return result
},
function error(error) {
console.error(error)
},
)

Variation 2:

add(5, 5)
.then(function success(result) {
return result
})
.catch(function(error) {
console.error(error)
})

However, these two examples aren’t exactly the same. In Variation 2, if we attempted to throw an error in the resolve handler, then we’d be able to retrieve the caught error inside the .catch block:

add(5, 5)
.then(function success(result) {
throw new Error("You aren't getting passed me")
})
.catch(function(error) {
// The error ends up here
})

In Variation 1, however, if we attempted to throw an error inside the resolve handler, we wouldnt be able to catch the error:


Conclusion

And that concludes this article. I hope you found this to be valuable. Look out for more in the future!

Exit mobile version