JavaScript — Callbacks, Promises and Lessons Learned

Natalio
edataconsulting
Published in
7 min readNov 18, 2020

--

Introduction

When I started working with asynchronous code in JavaScript, I didn’t really understand it, over the years I slowly got my head around it. With this article my hope is to make this topic easier for newcomers.

In order to do so, I will try my best to provide an introduction to callbacks and promises, two different solutions to deal with asynchrony. Finally, I would like to highlight some lessons I’ve learned over the time I’ve spent working with promises.

Some JavaScript knowledge is assumed, mainly functions and arrow functions.

Synchronous vs Asynchronous

Synchronous code in JavaScript is what you would expect in any other language, statements are executed in order, no matter how long one statement takes, the next one won’t start until the previous one finishes. Let’s see a very basic example of synchronous code:

Output

This code would represent how we usually do taks in our day to day, first we do the dishes, once we are finished we take a break and watch Netflix, and finally we vacuum the floor.

Now, let’s imagine we have a dishwasher and a robot vacuum, since these appliances work on their own, we can update our example to better reflect reality:

Output

As you can see, each task is now started right away, and finishes at its own pace. Following the example would be someone that sets the dishwasher, turns on the robot vacuum and goes to watch some tv shows, each of the three tasks will finish independently.

This is what we call asynchronous code, with this kind of code we now have a new problem: what happens if we want to do something when one of the tasks finishes, for instance, maybe we want to go to sleep after we finish watching Netflix. In order to solve this problem the first thing that we are going to see are callbacks.

Callbacks

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action. Let’s see an example, using setTimeout:

Going back to our example (now simplified), we can now see how we could go to sleep after our favorite TV show finishes:

This code produces what we wanted, yay!

Output

While callbacks are a valid and simple solution to coordinate asynchronous code, they have some drawbacks.

Readability

Once you start to chain them you end up with what is known as “callback hell”:

Code becomes hard to read, hard to debug, and easy to break.

Lack of Standardization

There is no hard rule that all callbacks should follow (while some may argue that Node.js style is the de facto standard), gathering results and error handling can become a nightmare, specially when it’s combined with the “callback hell”:

While this code could be improved adding named functions, the problem will still be there.

Promises

Promises are a better alternative to callbacks, a promise is an abstraction (built on top of callbacks) that represents the future result of an asynchronous operation. It has three possible states:

  • Pending, default initial state
  • Fulfilled, the operation was completed successfully, resolve was called,
  • Rejected, the operation failed, reject was called

Let’s see an example of how we can create Promises:

And also how we react to promises states:

Now that we know the basics, we can build a basic example of promise usage:

Output

There are a few things to notice:

  • As soon as the promise is created, the code inside will be run immediately (“Inside Promise -> Started waiting”)
  • Code will continue to run after the promise is created (“Meanwhile checking twitter”)
  • After the promise is fulfilled the callback specified on the “then” will be run (“I waited for one second”)

Chaining (Hell?)

Going back to the example we had, we will now see how it would be using promises, first let’s see the auxiliary functions:

And now the actual promise handling code:

Wait, what?! isn’t that exactly the same “ugly” code we had with callbacks? Well, yes, it is, the point here is that using promises doesn’t guarantee you better code, so, how should we code this to get it right?:

Much better, isn’t it?, this is one of the greatest benefits of promises, the ability to easily chain them, every time you create a “then” block, the result of that block will be passed along to the next then block, moreover if the result is a promise, that promise will be waited on, and the result of it passed along.

It is also worth mentioning how catch behaves when combined with chaining, when a promise is rejected, the next catch block in the chain will be called, if the catch does not throw a new error, from that point on, the promise becomes resolved again:

Standardized API

Promises also provide you a standard way of handling success and errors, the “then” and “catch” blocks provide you exactly that:

  • When a promise is fullfilled you get the result as an argument to your then code
  • When a promise is rejected you get one argument to your catch code, explaining the reason of the rejection

Lessons Learned

We’ve seen how asynchronous code introduces the need to coordinate tasks, our first option was callbacks and we quickly saw that they have some drawbacks. The better alternative, promises, makes our code easier to read and mantain. Now, to finish this off, I’d like to go over some lessons I’ve learned over the time I’ve been working with promises.

Avoid Promise Constructor

When I first started working with promises, I quickly fell on this one, every function looked like this:

This code is inefficient, harder to read than what it should, and error prone, it would be easy to forget about calling reject or resolve under certain conditions (with more complex scenarios). Let’s see the correct way of doing it:

Since fetch is already returning a promise there is no need for us to wrap that into a new promise and since we didn’t do anything with the error, besides passing it to the reject callback, we can completely skip that.

Another scenario where I found myself creating unnecessary promises was when wrapping synchronous code with promises, I tend to do this when I want to deal with both synchronous and asynchronous code transparently, or when I know that the code will probably evolve into using some asynchronous API:

This can be avoided using promise helper methods:

As a general rule, we should always avoid creating new promises, however there is one scenario that I find myself still needing to do it: when working with APIs that only support callbacks, it usually looks like this:

Note: This scenario can also be avoided using, for instance, bluebird or node promisify

Chaining Hell

Not only I was creating promises everywhere, I also started to chain them, but in the wrong way:

It doesn’t look that bad in the example, but with more complex scenarios and error handling added and I assure you it was a nightmare. Fixing this problem, when the promises do not depend on each other, or they only depend on the previous one, is easy enough:

It’s not always as easy as that, sometimes you end up having complex dependencies between your functions, what I normally do on those cases is to create an object where I store results as needed:

Promise Utils

Over time I got to know the utilities that promise provides, and I recommend you do the same, there are plenty of situations where you can use them to save you work:

Bonus: allSequential

This is one utility I have used in several different scenarios, it behaves as Promise.all but it runs one promise at a time, in order to do so, you first need to wrap your promise into something that can be lazily evaluated, a function:

Conclusion

We’ve seen how asynchrony introduces the need for coordination between tasks, we saw callbacks and promises as solutions to this need, and then we added some tips on working with promises.

If you have read this far, thanks. I hope you found the article useful, if you happen to find any typo or you disagree on some of my thoughts, please do comment, I’d be happy to engage on healthy discussions. Constructive feedback is also welcome.

--

--