As we already know, JavaScript has two ways to run code: Synchronous and Asynchronous. We use both depending on what we're building.
If you don't know what they are or why we need them, I recommend reading this blog first.
In this blog, we'll learn why Node.js uses async code, what callbacks are, why nested callbacks become a problem, and how promises make async code much easier to write and understand.
Topics Covered
- Why async code exists in Node.js
- Callback-based async execution
- Problems with nested callbacks
- Promise-based async handling
- Benefits of promises
Why Async Code Exists in Node.js
Before we start, let's quickly understand Node.js.
Node.js is a JavaScript runtime that lets us run JavaScript outside the browser. It's mainly used for backend applications where thousands of requests can come at the same time.
So why do we need async code?
Because it doesn't block the main thread. While one task is taking time, Node.js can keep handling other requests.
Think about a banking app. A function is fetching all users and their bank details from the database.
const users = getAllUsers(); // Takes 10 seconds
console.log("Checking account balance...");
If this is synchronous, "Checking account balance..." has to wait until getAllUsers() finishes.
With async code, Node.js doesn't wait. It keeps handling other requests while the database does its work.
That's why async programming is such a big deal in Node.js.
Callback-based async execution
Before we start this topic let's quickly understand what a callback is.
A callback is just a function that you pass to another function. That function can call it immediately or later. It depends on the use case.
// ↓ ↓
setTimeout(() => {console.log("hello")}, 2000);
This is a simple example of a callback. Here setTimeout is calling the arrow function after 2 seconds.
If you want to understand callbacks in detail I already wrote a blog on it.
Now we know what async code is and what a callback is. If we combine these two ideas we get callback based async execution.
It basically means:
Start an async task and keep the program running. Once the async task is complete execute the callback function.
Let's understand it with an example.
console.log("Start");
setTimeout(() => {
console.log("Task completed");
}, 2000);
console.log("End");
Output
Start
End
Task completed
Why is the output like this?
Because JavaScript keeps running the synchronous code(eg :- console.logs ) while the timer is running and executes the callback only after the timer is finished.
Problems with nested callbacks
Do you ever heard of the term "Callback Hell" it is a phenomenon where multiple callbacks are nested after each other
It happen when you do an asynchronous activity that's dependent on previous asynchronous activity
This make the code harder to read
Let look into the example :
loginUser("kunal", (user) => {
getProfile(user.id, (profile) => {
getPosts(profile.id, (posts) => {
getComments(posts[0].id, (comments) => {
console.log("Final Data:", comments);
});
});
});
});
pretty hard right ??
One solution to overcome callback hell is to break the callback function into smaller code or You can use promises and async / await
If you want to know i have a blog on it , read 👇
Promise-based async handling
As our code gets bigger, using callbacks everywhere becomes hard to manage. Sometimes it leads to callback hell, where callbacks keep getting nested inside each other.
To solve this problem, JavaScript introduced Promises.
A Promise represents a value that will be available now, later, or might fail.
Instead of nesting callbacks, we can chain async operations together, which makes the code much easier to read.
For example, instead of this:
loginUser("kunal", (user) => {
getProfile(user.id, (profile) => {
getPosts(profile.id, (posts) => {
console.log(posts);
});
});
});
We can write:
loginUser("kunal")
.then((user) => getProfile(user.id))
.then((profile) => getPosts(profile.id))
.then((posts) => {
console.log(posts);
})
.catch((err) => {
console.log(err);
});
Notice how the code is flat instead of deeply nested. That's one of the biggest reasons developers prefer Promises.
Benefits of Promises
Better error handling
Instead of handling errors in every callback, we can handle them in one place using .catch() or try...catch with async/await.
fetchData()
.then((data) => console.log(data))
.catch((err) => console.log(err));
Chaining
We can connect multiple async operations using .then() without creating nested callbacks.
loginUser()
.then(getProfile)
.then(getPosts)
.then(console.log);
Cleaner syntax with async/await
async/await is built on top of Promises. It lets us write async code that looks almost like normal synchronous code.
async function getData() {
try {
const user = await loginUser();
const profile = await getProfile(user.id);
console.log(profile);
} catch (err) {
console.log(err);
}
}
Run multiple tasks together
If two async tasks don't depend on each other, we can run them at the same time using Promise.all().
const [users, posts] = await Promise.all([
getUsers(),
getPosts(),
]);
console.log(users, posts);
This is faster because both requests start together instead of waiting for one to finish first.
Thanks for reading ! if enjoyed this blog , you can read more on this 👇
Top comments (0)