loading...
Cover image for How to Escape Callback Hell with JavaScipt Promises

How to Escape Callback Hell with JavaScipt Promises

amberjones profile image AmberJ ・4 min read

What's callback hell and what the hell are Promises?? To dive into those questions requires some basic understanding of the Javascript callstack, so I'll go into brief detail about that first and then navigate you through and out of callback hell.

Nature of the Beast

JavaScript is a single threaded language - meaning it has a single callstack and it can only execute one line of code at a time..

The callstack is basically a data structure which keeps track of what the program should run next. It follows the rules of FIFO - First In, First Out.

Step into a function call and it gets adds to the top of the stack, return a function and it pops off the top of the stack.

You wouldn't grab the waffle at the bottom of the stack. Neither would JavaScript.

So yeah, Javascipt has a single callstack. And this actually makes writing code simple because you don’t have to worry about the concurrency issues - or multiple computations happening at the same time.

Great!

...except when you do want stuff to happen at the same time. For example, writing web applications that make dozens of asynchronous calls to the network - you dont want to stop the the rest of your code from executing just to wait for a response. When this happens, its called holding up the event loop or "main thread".

Callback Hell

The first solution to work around JavaScript's single thread is to nest functions as callbacks.

It gets the job done, but determining the current scope and available variables can be incredibly challenging and frustrating.

And it just makes you feel like:

When you have so many nested functions you find yourself getting lost in the mist - this is whats referred to as callback hell. Its scary and no one wants to be there!

Nested callbacks tends to develop a distinct pyramid shape -

fightTheDemogorgon(function(result) {
  rollForDamage(result, function(seasonsLeft) {
    closeTheGate(seasonsLeft, function(finalResult) {
      console.log('Hawkins is safe for ' + finalResult + ' more seasons.');
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

And just imagine this happening even further, with 10 or 15 more nested functions calls. SCARY RIGHT??

JavaScript developers recognized this was a problem, and they created Promises.

Introduced in ES6 (2015), a Promise is an alternative way to format your asynchronous functions without breaking the event loop. It returns a special promise object that represents a future result.

Whats the Difference?

A lot of it is formatting.

Callbacks do not return anything right away, they take a function as an argument, and then you tell the executing function what to do when the asynchronous task completes.

Promises on the other hand immediately return a special promise object. They do not need a function argument, thus it does not need to be nested.
You provide the action to be taken when the asynchronous task completes using a promise method called then().

Chaining, aka the Power of Friendship

Alt Text

The truly AWESOME thing about Promises is that they can be chained by using their then() method when we need to execute two or more asynchronous operations back to back.

Each chained then() function returns a new promise, different from the original and represents the completion of another asynchronous step in the chain.

You can basically read it as Do this, THEN do this, THEN this.

Promises also have a catch() method. Chaining a catch() to end of a chain will give you the errors for any failed promise in the chain. Its also useful to set an action to take in the event of a failure in the chain.

Promise chaining allows us to get rid of the nasty nesting callback pattern and flatten our JavaScript code into more readable format.

fightTheDemogorgon()
.then(function(result) {
  return rollForDamage(result);
})
.then(function(seasonsLeft) {
  return closeTheGateIn(seasonsLeft);
})
.then(function(finalResult) {
  console.log('Hawkins is safe for ' + finalResult + ' more seasons.');
})
.catch(failureCallback);

With ES6 syntax we can condense this even further!

fightTheDemogorgon()
.then((result) => rollForDamage(result))
.then((seasonsLeft) => closeTheGateIn(seasonsLeft))
.then((finalResult) => console.log('Hawkins is safe for ' + finalResult + ' more seasons.'))
.catch(failureCallback);

Defeating the Beast, Escaping Hell

The beast here being asynchronous calls, and hell being callback hell.

There is nothing stopping you from nesting Promise functions in typical callback fashion. But it's not necessary! This is usually accidentally self inflicted and is just a lack a familiarity with Promises.

You can think of Promises as callbacks in fancy new clothes. It allows asynchronous code to look cleaner, promotes ease of use and readability, most importantly, it gives you a way out of callback hell.

Alt Text

There is an even newer method called Async/await introduced in ES8 (2017). Check it out!

Thanks for reading!

References:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://www.youtube.com/watch?v=8aGhZQkoFbQ

Discussion

pic
Editor guide
Collapse
kedar9 profile image
Kedar

Nice article.
Another clean "solution" I'd like to add is to use async/await:

const summerActivities = async () => {
  try {
    const result = await fightTheDemogorgon();
    const seasonsLeft = await rollForDamage(result);
    const finalResult = await closeTheGateIn(seasonsLeft);
    console.log('Hawkins is safe for ' + finalResult + ' more seasons.'));
  } catch (e) {
    failureCallback();
  }
}

Collapse
sebbdk profile image
Sebastian Vargr

This ^

And' it supported by major browser now. :D
caniuse.com/#feat=async-functions

The poly-fills for async/await can cost a lot of kb depending on how it's transpiled and chunked.

So keep that in mind if speed and legacy is a factor.

Collapse
amberjones profile image
AmberJ Author

Thanks for the addition! 😎

Collapse
vonheikemen profile image
Heiker

When there is no way to avoid callback style functions, named functions can help ease the pain.

function fight(result) {
  rollForDamage(result, keepFighting, failureCallback);
}

function keepFighting(seasonsLeft) {
  closeTheGate(seasonsLeft, finishHim, failureCallback);
}

function finishHim(result) {
  console.log('Hawkins is safe for ' + result + ' more seasons.');
}

fightTheDemogorgon(fight);

Also, if you are using node you can use the promisify utility to convert those to promise based functions.

Collapse
amberjones profile image
AmberJ Author

Definitely! Love some bluebird and util. Thanks for commenting Heiker!

Collapse
molamk profile image
molamk

Great article! We can condense that even further (no need to write the arguments here)

fightTheDemogorgon()
.then((result) => rollForDamage(result))

Becomes

fightTheDemogorgon()
.then(rollForDamage)

Chaining, aka the Power of Friendship

That's just genius πŸ˜‚

Collapse
0x12b profile image
Simon Aronsson

One thing worth noting regarding the shortform though, is that

fightTheDemogorgon()
  .then(rollForDamage)

actually becomes something along the lines of

fightTheDemogorgon()
   .then(function(result) {
   });

rather than

fightTheDemogorgon()
   .then((result) => ...);

which might come with unintended side effects to function scoping and what this actually points to.

Collapse
amberjones profile image
AmberJ Author

Nice catch!

Collapse
pavlosisaris profile image
Paul Isaris

Awesome post, Amber! Loved the parallelism with Stranger Things... :D

Collapse
amberjones profile image
AmberJ Author

Thanks Paul!

Collapse
atulvermaon18 profile image
Atul

Promises are outdated now, should check with async/await :)

Collapse
amberjones profile image
AmberJ Author

you should write a post about it then

Collapse
atulvermaon18 profile image
Atul

I would but that too is an old story.