This article is aimed at people starting out with asynchronous coding in javascript so we would keep things simple by avoiding big words, arrow functions, template literals etc.
Callbacks are one of the most used concepts of modern functional javascript and if you've ever used jQuery, chances are you've already used callbacks without even knowing (we will get back to it in a minute).
What the Heck are Callback Functions?
A callback function in its simplest terms is a function that is passed to another function, as a parameter. The callback function then gets executed inside the function where it is passed and the final result is returned to the caller.
// I'm sure you've seen a JQuery code snippet like this at some point in your life!
// The parameter we're passing to the `click` method here is a callback function.
$("button").click(function() {
alert('clicked on button`);
});
Simple right? Now let us implement a callback function to get scores on levelling up in an imaginary game.
Once inside startGame()
function, we call the levelOne()
function with parameters as currentScore and our callback function().
When we call levelOne()
inside startGame()
function's scope, in an asynchronous way, javascript executes the function levelOne()
and the main thread keeps on going ahead with the remaining part of our code.
This means we can do all kind of operations like fetching data from an API, doing some math etc., everything which can be time-consuming and hence we won't be blocking our main thread for it. Once the function(levelOne()
) has done with its operations, it can execute the callback function we passed earlier.
This is an immensely useful feature of functional programming as callbacks lets us handle code asynchronously without us have to wait for a response. For example, you can make an ajax call to a slow server with a callback func. and completely forget about it and continue with your remaining code. Once that ajax call gets resolved, the callback function gets executed automatically.
But Callbacks can get nasty if there are multiple levels of callbacks to be executed in a chain. Let's take the above example and add a few more levels to our game.
Wait, what just happened? We added two new functions for level logic, levelTwo()
and levelThree()
. Inside levelOne's callback(line #22), called levelTwo() function with a callback func. and levelOne's callback's result. And repeat the same thing for levelThree() function again.
Now just imagine what this code will become if we had to implement the same logic for another 10 levels. Are you already panicking? Well, I am! As the number of nested callback functions increases, it becomes tougher to read your code and even harder to debug.
This is often affectionately known as a callback hell. Is there a way out of this callback hell?
I Promise there's a better way
Javascript started supporting Promises from ES6. Promises are basically objects representing the eventual completion (or failure) of an asynchronous operation, and its resulting value.
// This is how a sample promise declaration looks like. The promise constructor
// takes one argument which is a callback with two parameters, `resolve` and
// `reject`. Do something within the callback, then call resolve if everything
// worked, otherwise call reject.
var promise = new Promise(function(resolve, reject) {
// do a thing or twenty
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
Let us try to rewrite our callback hell example with promises now.
We have re-wrote our level(One/Two/Three) functions to remove callbacks from the function param and instead of calling the callback function inside them, replaced with promises.
Once startGame is resolved, we can simply call a .then()
method on it and handle the result. We can chain multiple promises one after another with .then() chaining
.
This makes the whole code much more readable and easier to understand in terms of what is happening, and then
what happens next and so on.
The deep reason why promises are often better is that they're more composable, which roughly means that combining multiple promises "just works" while combining multiple callbacks often doesn't.
Also when we have a single callback versus a single promise, it's true there's no significant difference. It's when you have a zillion callbacks versus a zillion promises that the promise-based code tends to look much nicer.
Okay, we've escaped successfully from the callback hell and made our code much readable with promises. But what if I told you there's a way to make it cleaner and more readable?
(a)Wait for it
Async- await is being supported in javascript since ECMA2017. They allow you to write promise-based code as if it were synchronous code, but without blocking the main thread. They make your asynchronous code less "clever" and more readable.
To be honest, async-awaits are nothing but syntactic sugar on top of promises but it makes asynchronous code look and behaves a little more like synchronous code, that's precisely where it's power lies.
If you use the async
keyword before a function definition, you can then use await
within the function. When you await
a promise, the function is paused in a non-blocking way until the promise settles. If the promise fulfils, you get the value back. If the promise rejects, the rejected value is thrown.
Let us see now how our game logic looks once we rewrite it with async-awaits!
Immediately our code becomes much more readable but there's more to Async-await.
Error handling is one of the top features of Async-await which stands out. Finally we can handle both synchronous and asynchronous errors with the same construct with try and catches which was a pain with promises without duplicating try-catch blocks.
The next best improvement from good old promise world is code debugging. When we write arrow function based promises, we can't set breakpoints inside our arrow functions so debugging is tough at times. But with async-awaits, debugging is just like how you would do a synchronous piece of code.
I'm sure that by now you have a better understanding of asynchronous programming in javascript. If you have a question, let me know below. If you found this helpful, give me a shoutout on Twitter!
Happy Coding! ✌️
Top comments (33)
Nice article. Good job.
One thing that is worth mention is that callback hell can be avoided even if you are "stuck" in an environment that doesn't support promises or async/await. You can always use a function reference instead of an anonymous function.
The game example could look like this:
As someone who doesn't regularly code JS, this was a very helpful primer for these features. Thanks!
Glad to hear that Zeke! 🙂
In the last example, do you really need
in each function, or you can simply return?
By the way, great article!
I think the author was returning those
Promise
s explicitly here to illustrate the point of being able to "get the value out of" thePromise
using theawait
keyword, without having to jump through the normal.then(fn)
hoops. An alternative (usually considered to be more idiomatic) way to write the return statements would have beenreturn Promise.resolve(newScore);
.However, without any "real" async logic in those functions, you could indeed simply return and it would preserve the current behaviour; you can
await
a regular value just fine (i.e.let x = await 5
is valid syntax). As far as I'm aware it can potentially affect the order in which things are processed within the current iteration of the event loop, but that won't observably influence things in most cases, including this one.For completeness' sake, we could take things one step further by making the
level
functionsasync
as well. Anasync
function will always return aPromise
implicitly, so the following are functionally equivalent:This way, we can leave the explicit
Promise
s out, but still benefit from the asynchronous flow.To make the asynchronous behaviour clearer, we could introduce a "wait" function as well, which does nothing more than resolve the
Promise
it returns after the passed amount of milliseconds:If you run this code (e.g. paste it into your console and press enter), you'll see that it runs just as the code in the article, the difference being it has 500ms pauses in between the log messages (the number 500 coming from the value that is passed to
wait
in each case).Note that if you were to remove the
await
keywords in thelevel
functions, that would take away the 500ms pause in that spot. Thewait
function would still be called, but the engine is not instructed to wait for the returnedPromise
to resolve before continuing, so it will just move on to the next statement immediately.I couldn't have explained this question better @Joep 😀Thank you 🙏
My pleasure!
Well done!
Thank you 🙌
I've been struggling to wrap my head around promises and this article made it very clear with your example. Time to go write something with async/await 😀
Thank you Andrew, good luck ✌️
Good and perfect!
Thanks José 🙌
Nice article, very succinct and concise.
Yay! Learnt a new word, now I have to use it! 🎉🎉
Glad you found it succinct Luis 🤓
To anyone exploring asynchronous Javascript programming, also take a look at Observables, in the Rxjs library.
Thank you for this well written article. I am sure many will find it useful.
Thanks Jochem! :)
Really useful! Thank you!
I'm happy that you found this helpful.