Master Async Patterns in Node.js: From Callbacks to Async/Await

Have you ever struggled with nested functions or found the ordering of code execution confusing, then this is for you. Knowing about Async Patterns in Node.js is very important for anyone looking to write efficient code.

Consider that you are at a coffeehouse. You place an order for a latte. In a synchronous world, all activity at the coffeehouse would come to a complete stop while the barista grinds your espresso beans, steamed your milk, and created your design. Not a single person can place an order while this process is taking place.

Handling things in Node.js is a totally different story. Here, you place an order, and the barista gives you a ticket. Then, you move aside. The barista receives another order, and during this time, your coffee is brewed in the machine. Then, when the bar is ready, they call your name. It’s not blocking, and for this reason, Node.js is exceptionally fast. In this blog, we will discuss how we could handle these tasks and proceed from old ways to modern ways.

The old way: Callback Era

In starting, Node.js handled asynchronous operations using callbacks. A callback is a function you pass to another function to execute later. It works well for simple tasks but becomes harder for complex logic.

Let’s look at a scenario where we need to find a user in a database and then find their specific permissions. Here is how we structure the simulation functions.

function findUser(id, callback) {
    setTimeout(() => {
        console.log("User found")
        callback({ id: id, username: "DevMaster" })
    }, 1000)
}

function getPermissions(user, callback) {
    setTimeout(() => {
        console.log("Permissions found")
        callback(["admin", "editor"])
    }, 1000)
}

It looks simple. However, when you use these functions together, the nesting begins. Developers often search for how to avoid callback hell in node.js after writing code that looks like this:

console.log("Start")

findUser(1, (user) => {
    getPermissions(user, (permissions) => {
        console.log("Displaying permissions")
        console.log(permissions)
    })
})

console.log("End")

The logs will show Start and End immediately, followed by the user and permissions later. As you add more steps, this pyramid grows to the right, making it difficult to read and debug.

The Promise Solution

Promises arrived to flatten this pyramid. A Promise represents a future value. It effectively says, “I promise to return a result later, or I will tell you why I failed.” This pivotal shift in callback vs promise vs async await javascript changed how we structure applications.

First, we must modify our functions to return a Promise instead of accepting a callback.

function findUserPromise(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("User found via Promise")
            resolve({ id: id, username: "DevMaster" })
        }, 1000)
    })
}

function getPermissionsPromise(user) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("Permissions found via Promise")
            resolve(["admin", "editor"])
        }, 1000)
    })
}

Now you can consume these functions using chaining. It keeps the code vertical rather than horizontal.

console.log("Start Promise Chain")

findUserPromise(1)
    .then((user) => {
        return getPermissionsPromise(user)
    })
    .then((permissions) => {
        console.log(permissions)
    })
    .catch((error) => {
        console.error(error)
    })

It’s keeps code much better to read. We catch errors in one spot at the end. However, we still have functions inside .then() blocks, which can feel cluttered.

Async/Await Mastery

The industry now favors the syntax found in any modern async await in node.js. We use async and await to consume Promises in a way that looks synchronous. It creates the most readable code possible. You mark a function with async, and then use await to pause execution within that function until the Promise resolves. The thread remains free for other tasks, but your code reads top-to-bottom.

Here are node js async await examples step by step using the same Promise-based functions we defined earlier:

async function runAuthFlow() {
    console.log("Start Async Flow")

    try {
        const user = await findUserPromise(1)
        const permissions = await getPermissionsPromise(user)
        
        console.log("Final Result:")
        console.log(permissions)
    } catch (error) {
        console.error("Something went wrong")
        console.error(error)
    }
}

runAuthFlow()

This is the gold standard. We use a standard try/catch block for errors, just like in synchronous programming. There are no chains and no callbacks. The variables user and permissions are available directly in the scope.

Callbacks Promises and async await in Node.js Example

To wrap up on how to use callbacks promises and async await in node js, let’s look at a realistic fetch operation using the modern fetch API which returns promises natively.

async function fetchUserData(userId) {
    try {
        const response = await fetch(`https://api.example.com/users/${userId}`)
        
        if (!response.ok) {
            throw new Error("Network response failed")
        }

        const data = await response.json()
        console.log(data)
        return data
    } catch (error) {
        console.error("Fetch operation failed")
    }
}

This clean syntax makes maintenance easy. If you need to add a step, you simply add another line with await.

Summary

We has traveled through the history of Async Patterns in Node.js. From the simple callback, through recognition of the problem of nesting, from the more elegant form of Promises, to the beauty of Async/Await.

By learning to handle all these patterns, you would be able to code proficiently on Node.js. We would advise that you start practicing Node.js async/await examples to ensure that you develop nodejs async await functionality. If you begin refactoring your code already today, you would certainly see positive results.