Explaining promises and callbacks while implementing a sorting algorithm.

JavaScript

There is a good chance that you are already using promises in your code – they are everywhere now. Have you ever considered how they work, though? I this article I want to explain why they are a good thing and what problems do they solve. I will do that while implementing quite a ludicrous sorting algorithm. Let’s start!

Callbacks

The basic concept to understand here is that code in JavaScript can be asynchronous. I explained what does that mean and how it differs from multiple threads in my other article:

Imagine making an Ajax request to fetch some data from the server. You won’t get a response immediately – you need to wait a little bit for the server to respond. What happens then is that other parts of your code are running while waiting for the response. If it weren’t for that, the interface would freeze, waiting with interpreting the rest of your code to the moment you get the response back

It wasn’t always dealt with using promises. Back in the old days, we used callbacks. They are functions that we set to be called when the specified action is finished.

In this example, I created a function called sort that accepts three arguments: an array of numbers and two callbacks. It creates a new array of numbers and uses setTimeout to wait with pushing the numbers to it: the bigger the number, the longer it waits. When it is finished, the onSuccess callback is called. If there are any non-integer numbers found (it won’t work properly for non-integers, unfortunately), the function calls the onFailure callback. In the process, we get a new, sorted array! A condition   will be met only if all of the numbers are valid non-integers.

The problem

Callbacks are far from being perfect, though. Imagine wanting to do a sequence of operations using callbacks:

What we have here is a classic example of callback hell. Doesn’t really look good, does it?

Promise

Its name actually tells us a lot about it. You can think about it as a promise of a future value, a representation of a value not yet available. It can be in one of three states:

  1. Pending – the value is not available yet
  2. Fulfilled – the value became available
  3. Rejected – error prevented the value from being determined

An often brought up analogy is a situation in a restaurant: imagine ordering a cup of coffee. Right after you pay for it, its status is set to pending, and you are promised a coffee in a few minutes. You wait for your cup, doing some other stuff in the meantime, like watching funny cat videos on the web (you are not wasting your time, just like a web browser). Then a few things can happen: you can either get your coffee (what would set the status of said promise to fulfilled), or receive a sad news with an explanation, that they run out of coffee grains, for example, setting the status of a promise to rejected.

A good example of how the promises work is an implementation of a sleep function that is available in many other programming languages, such as C++.

As you can see, the promise constructor takes a function. Its arguments are resolve and reject, which are both functions also. First is called when a promise is fulfilled, the second one – when it is rejected.

Promise.prototype.then

It takes up to two arguments. The first one is a callback function that is going to be called on success. The second one is a callback function for failure. Then function returns a promise.

Promise.prototype.catch

It deals with rejections. It also returns a promise.

It behaves the same as calling Promise.prototype.then(undefined, onRejected)
(in fact, calling obj.catch(onRejected) internally calls obj.then(undefined, onRejected)).

It means that  is basically the same as  with only second argument provided.

Keep in mind that    is not the same as   .

In this example, the first error won’t be caught, because the failure callback is only for the   function.

Chaining promises

We can easily imagine a situation in which we need to make a few async calls one after another. I’m using Fetch API here.

It is starting to look like a callback hell again, isn’t it? But I’ve just said that both then and catch return promises. Thanks to that, we can call those functions again, creating a chain.

We can go even further and don’t store every promise in a variable.

That certainly looks better, isn’t it?

In the result, first, the users will be fetched. When that it’s finished, the companies will be fetched. It is important to notice that a return value of a callback is not the same as a return value of a then function itself: what you return from your callback is then wrapped in a new promise. Example of that is returning   above, and calling   again.

Remember to add return keywords before fetching more data in callbacks in those cases: no doing it would result in the next callback being called before the data is fetched.

Promise.all

It is also very easy to run a few promises in parallel and wait for all of them to be fulfilled. The solution to this is Promise.all. It takes an array of promises (that will run in parallel) and returns a single promise that will be resolved when all of the promises fron an array are resolved.

In this example, the users and companies will be shown in the console when both of them are fetched.

The solution

With all that knowledge, we can rewrite our sort function to use promises.

And this is how we got rid of a callback hell. If anywhere along the chain you pass a non-integer value such as   , all of the  callbacks below the failure would be omitted and it would go straight to the   callback.

Other details to keep in mind

Using catch, Promise.resolve and Promise.reject

In real situations, you should not forget to add   and handle errors, especially if you are communicating with some API. Remember that it returns a promise too and you can use that.

In this example, if fetching users fails, the function tries to use some data that might have been cached before. If none are found, then   returns Promise.reject() which will cause the last callback not to be called because a newly returned promise will be rejected at once. In this case, I am also using Promise.resolve because function getCatchedUsers is not asynchronous and to call   we need a promise. Returning  will case a new promise to be returned and immediately resolved.

Adding callbacks to promises long after they are resolved

This might be a little surprising to you. Check this example out:

As you can see, you can add callbacks using  and   even after the promise itself was resolved or rejected.

Promises fall through

There is an interesting catch to promises. Examine this example:

If you think the result will be “cat“, you are mistaken! Passing a non-function (such as promise, for example) to  function, causes it to be interpreted as   .  It will cause the previus promise’s result to fall through. An easy fix to that is this:

Promise.prototype.finally

It is quite a new function (it reached stage 4 in January this year). Passing a callback to it will cause it to be run whether the promise was fulfilled or rejected. It means that finally is similar to calling   . It also returns a promise.

In this example, isLoading will always be set to false at the end.

Summary

Although this sorting algorithm is not at all suitable for real-life use, I hope you learned something along the way. Async and promises can be tricky, but it is such an important feature of the JavaScript language, that we can’t escape from it. Embrace it, then!

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments