Solve Callback Hell with Composition

John Lindquist
InstructorJohn Lindquist
Share this video with your friends

Social Share Links

Send Tweet
Published 4 years ago
Updated 3 years ago

Many APIs in JavaScript rely on callbacks, so when using APIs together, we end up in deeply nested callbacks AKA "Callback Hell". This course solves callback hell through composition by moving all of the nesting into the internals of our helper functions which can expose a clean API that becomes a list of functions in a pipe.

John Lindquist: [0:00] Callback hell is the situation that occurs when you have way too many callbacks nested inside of each other. As an example, I'll set up document addEventListener. When I click on this, this will call this callback.

[0:15] Then a setTimeout that invokes a callback after one second, and then a fetch, which can grab my GitHub user id. Then the fetch to get the data needs to grab the JSON and then invoke a callback with that, so data, and we'll just console.log the data. If I click on the document, after one second, you'll see the data up here.

[0:44] Right now, we have a callback inside of a callback, inside of a callback, which is required because this click starts a timeout, which starts the fetch. Then inside of this callback, this could continue on based on your requirements, logic, and behaviors in an infinite number of ways.

[1:04] We can solve this with composition by thinking about how we can break this into functions. I'm going to extract fetch and put it into a function called getURL() and just paste it in there. I'll extract setTimeout. I'll put this into a function called timeout() and paste that in there. My click, I'll call this function click() and just paste that inside of there.

[1:28] Each of these behaviors are inside of individual functions. We just have to figure out how to wire them up.

[1:34] The relationship of nesting is essentially you invoke an outer function, which takes a callback. Then inside of that, it calls an inner function, which takes a callback. We can even define this behavior inside of a function called nest().

[1:52] This will take our inner, or the function that's being nested, inside of an outer, which is the function that wraps around. Then finally we need a listener as the function that is the callback. I can paste this inside, where it takes this, sets up a temporary callback, and then invokes inner. We can now pass our listener into the inner function. Right now, we're ignoring the value, but we'll touch on that in the next lesson.

[2:21] To be able to wire these up, it would look something like I start with a click, and then inside of the click, I'm going to call my nest function, where we want to nest the timeout. Then around that I can call my nest function around this function, and this one calls the getURL.

[2:44] This is the implementation. It won't work yet. You'll see if I click, nothing happens, because what it's missing is this listener. The listener we're using is this one right here. This is our callback. Let's extract that and pass that in here. This means that this listener is this one here, which will go into here. First of all, this listener will get passed into click. I'll put it right here.

[3:09] Replace this event with listener, then that listener will get passed down to timeout. I'll replace this with listener. Then that listener gets passed down to getURL. We'll replace that with listener and then drop the listener in here.

[3:25] Know we have everything wired up, except that I missed a paren here. I need to put this on the other side of click. Let's save, and once I click, you'll see it's back to waiting for one second and loading our data.

[3:40] We've seen this mess of parens before. As you saw, I already messed up on where one paren goes, and this is difficult to read. We've also already solved this through composition.

[3:51] If I import, you come up here, and import from lodash/fp. I'll import our pipe, then I can say pipe and start pulling these functions out. The first thing after the click is the timeout, so we'll cut that, paste it here, comma, the next thing after the click is the getURL.

[4:16] We'll cut that and paste it there, your getURL. I can name this a function like timeoutURL(), and just use that around my click. I'll say timeoutURL. Now when I hit Save, I'll click, it waits one second, and loads the data.

[4:37] Most people getting into JavaScript are used to writing functions that do things, such as logging or adding numbers or parsing strings.

[4:45] The more advanced you get, the more you find yourself writing functions to organize other functions, to help your code be a more readable and well-named, so that once you actually write the line that implements it, it will read like once I click then timeout, getURL, and log out the data.

James
James
~ 4 years ago

How do we figure out that this is the form that's needed? We invoke nest with the first param as one of the functions we took out but the second param is also nest but this time with two params. It's very confusing how we ended up with the code below plus how the order is figured out. Can anyone explain some more please?

nest(getURL)(nest(timeout)(click))((data) => { console.log(data) })

Alex Jacobs
Alex Jacobs
~ 4 years ago

+1 for it being confusing.

I plan to try to reproduce the steps without looking to try and make a mental map of how this final composition was created

Alex Jacobs
Alex Jacobs
~ 4 years ago

Also just saw that there’s a link in the notes for lesson 8. It points to the next set of lessons where these concepts are built up more gradually

~ 4 years ago

I think it is much harder to read doing this way.

Richard Dyce
Richard Dyce
~ 4 years ago

+1 for functionA(functionE)(functionB(functionC)(functionD)) being confusing

James
James
~ 4 years ago

I think it is much harder to read doing this way. I would still like to understand how we get there before simplifying with lodash

Andrea Di Marco
Andrea Di Marco
~ 3 years ago

How do we figure out that this is the form that's needed?

Based on the function you are trying to break and recompose. If you look closely, nest is reproducing the same kind of contract... The outer function is passing an event that is ignored, which is the value that gets passed to inner and so on. So nest is nothing magical and its implementation will depend on the problem at hand. The thing to stick in mind is that you break your starting callback hell into functions and then create an operator that describes what you want to happen.

I'm a bit confused by the signature of John's nest function and the reason why he starts accepting inner before outer and not the other way around. It's just harder to reason about since you have inner and then outer before you jump back to the final listener. Just preference?

John Lindquist
John Lindquistinstructor
~ 3 years ago

Thanks for all the feedback!

Re: Confusion with the lesson

Please note that this lesson is mostly a setup for future lessons where we start using this pattern over and over.

The core takeaways of this should be:

  1. extract nested callbacks
  2. use a pattern to manage callbacks

Don't worry about fully understanding the pattern now as it is covered many times over in future lessons with much better examples showing the value of the pattern. Even then, this is a difficult and complex subject tackled by many different frameworks, approaches, and tradeoffs. These lessons are essentially building a framework from scratch where you're seeing the ugly internals and learning how they work.

Lastly, this example is far too simple to fully represent the value of handling multiple streams of events, but I had to start somewhere. The best advice I have for each of these comments is "please keep watching the next lessons". Thanks for sticking with it and giving this course some of your valuable attention.

Markdown supported.
Become a member to join the discussionEnroll Today