For understanding the concept of callbacks and callback hell, I think you should know about Synchronous and Asynchronous programming in JavaScript(or any other language). Let's see a quick view on these topics in context of JavaScript.
Synchronous Programming
It is a way of programming in which you can perform only one task at a time and when one task is completed we move to another task. This is what we called Blocking Code operation because you need to wait for a task to finish to move to the next one.
console.log("Program Starts");
let sum = getSum(2,3);
console.log(sum);
console.log("Program Ends");
In the above code snippet, you see code will execute line by line and when an operation on one line is finished then we move to the next line so this is just a simple example of the synchronous way of programming and we do this in our daily life of programming.
Asynchronous Programming
Asynchronous programming allows you to perform that work without blocking the main process(or thread). It’s often related to parallelization, the art of performing independent tasks in parallel, that is achieved by using asynchronous programming.
In asynchronous operation, you can move to another task before the previous one finishes, and this way you can deal with multiple requests simultaneously.
In JavaScript, a good example of asynchronous programming is setTimeout
function, let's see a quick example -
console.log("Program Starts");
setTimeout(() => {
console.log("Reading an user from database...");
}, 2000);
console.log("Program Ends");
So, the output of this program will look like -
Program Starts
Program Ends
Reading an user from database...
Pretty cool, right? Our program didn't wait for setTimeout
to finish, just goes for the next line, then came back to the function and prints the output. This is what we called Non Blocking code. You can read more about it here.
There are three design patterns in javascript to deal with asynchronous programming -
- Callbacks
- Promises
- async/await (just a syntactical sugar of promises)
Callbacks
Callbacks is a great way of handling asynchronous behavior in javascript. In JavaScript, everything behaves like an object so functions have the type of object and like any other object (strings, arrays, etc) you can pass functions as an argument to other functions and that's the idea of callback.
function getUser(id, callback) {
setTimeout(() => {
console.log("Reading an user from database...");
callback({id: id, githubUsername: 'jerrycode06'});
}, 2000);
}
getUser(1, (user) => {
console.log("User", user);
})
You see, we are passing function as an argument to getUser
function and it calls inside the getUser
function, output will look like -
Reading an user from database...
User {id: 1, githubUsername: 'jerrycode06'}
Callback Hell
In above code snippet, we are getting user with github username now let's suppose you also want repositories for that username and also commits in the specific repository so what can we do with the callback approach -
getUser(1, (user) => {
console.log("User", user);
getRepositories(user.githubUsername, (repos) => {
console.log(repos);
getCommits(repos[0], (commits) => {
console.log(commits);
// Callback Hell ("-_-)
}
})
You are seeing now a nesting of functions here and code also looks scary and this is what we called Callback Hell. For a big application it creates more nesting.
To avoid this, we will see now Promises.
Promises
Promises are the alternative to callbacks for delivering the results of asynchronous computation. They require more effort from implementors of asynchronous functions, but provide several benefits for users of those functions. They are more readable as compared to callbacks and promises has many applications like fetch
in javascript , mongoose
operations and so on. Let's see how to implement promises with above example. Actually, promises have four states -
- fulfilled - The action relating to the promise succeeded
- rejected - The action relating to the promise failed
- pending - Hasn't fulfilled or rejected yet
- settled - Has fulfilled or rejected First we have to create promises to understand this -
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Reading from a database....");
resolve({ id: id, githubUsername: "jerrycode06" });
}, 2000);
});
}
function getRepositories(username) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Extracting Repositories for ${username}....`);
resolve(["repo1", "repo2", "repo3"]);
// reject(new Error("Error occured in repositories"));
}, 2000);
});
}
function getCommits(repo) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Extracting Commits for " + repo + "....");
resolve(["commits"]);
}, 2000);
});
}
We created three functions, instead of passing callback function we are now returning a Promise which has two argument resolve and reject. If everything worked, call resolve
otherwise call reject
. Let's see how to use promises -
// Replace Callback with Promises to avoid callback hell
getUser(1)
.then((user) => getRepositories(user.githubUsername))
.then((repos) => getCommits(repos[0]))
.then((commits) => console.log("Commits", commits))
.catch((err) => console.log("Error: ", err.message));
More readable, Isn't it? Using arrow functions made this less complex than using simple functions. We have avoided nesting of functions and reduce the complexity of code (callback approach) and that's how promises work. You can deep-dive more about promises here.
async/await
It is supposed to be the better way to write promises and it helps us keep our code simple and clean.
All you have to do is write the word async
before any regular function and it becomes a promise. In other words async/await
is a syntactical sugar of using promises it means if you want to avoid chaining of then()
methods in promises, so you can use the async/await
approach but internally it also uses the chaining.
Let's see how to implement it with above example -
// Async- await approach
async function displayCommits() {
try {
const user = await getUser(1);
const repos = await getRepositories(user.githubUsername);
const commits = await getCommits(repos[0]);
console.log(commits);
} catch (err) {
console.log("Error: ", err.message);
}
}
displayCommit();
Now, it is more readable than using promises above. Every time we use await
, we need to decorate this with a function with async
. Like promises, we don't have catch()
method here so that's why we are using try-catch
block for the error handling.
Conclusion
In this article we've seen -
- Synchronous vs Asynchronous
- Callbacks and Callback Hell
- Avoid Callback hell with promises and async/await
I personally like the async/await approach the most but sometimes we should the promises approach to deal with async behaviour.
Thanks for reading this long post! I hope it helped you understand these topics a little better. If you liked this post, then please do give me a few ❤️ and share it if you can. You are welcome to
give any suggestions in comments and ask anything!
Top comments (10)
Nice article, check this out npmjs.com/package/await-handler
Using this we can skip the try catch block and handle error there it self. Like in case you want to show different error message for different errors, one way to go.
Nice, Thanks for sharing this!!
Thanks, enjoyed reading it!
Nicely written!!
"I think you should know about Synchronous and Asynchronous programming in JavaScript(or any other language)"
Except that no ... the three patterns of Async programming you describe don't apply to any other language except Javascript.
While ReactPHP has some things that resemble callbacks, just about any other async-capable language uses multithreading, either soft threads (managed by the platform such as Go's goroutines or PHP Swoole's coroutines), system threads or full-blown parallelism.
For a fully relevant discussion on async programming, it might be better to start with concurrency vs parallelism and then see what JS is capable of.
This article basically focuses on callback hell and I have given just a quick overview of async programming, although I agree on your point -
"the three patterns of Async programming you describe don't apply to any other language except Javascript."
Thank you for mentioning it, I should've added it to my article.
Great piece :-)
Thanks ;-)
Awesome piece~ Thanks for the detailed yet clear and practical post
Glad you liked it.