Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

Published
4 min read
Async Code in Node.js: Callbacks and Promises
A
my work defines me

Imagine you are at a busy coffee shop. If the barista took your order, started brewing the coffee, and stood perfectly still waiting for the machine to finish before talking to the next customer, the line would stretch out the door.

Instead, they take your order, give you a buzzer, and immediately help the next person. That is exactly how Node.js handles tasks. It doesn’t like waiting around.

Here is what we are going to explore today:

  • Why asynchronous code is the heart of Node.js

  • How callbacks work (The "Buzzer" system)

  • The nightmare of "Callback Hell"

  • Promises: A cleaner way to handle the future

  • Why Promises are a major upgrade for your code


Why Async Code Exists in Node.js

Node.js is built to be non-blocking. Most computer tasks, like reading a large file from a hard drive or fetching data from a database, are slow compared to the speed of the CPU.

If Node.js ran "synchronously," the entire server would freeze while waiting for a file to load. No other users could connect. Asynchronous code allows Node.js to start a task, move on to something else, and come back when the task is finished.


Callback-Based Execution

A callback is simply a function passed as an argument to another function. It’s like saying: "Go read this file, and once you're done, run this specific piece of code."

Example: Reading a file

const fs = require('fs');

console.log("Starting file read...");

fs.readFile('message.txt', 'utf8', (err, data) => {
    if (err) {
        console.error("Oops! Something went wrong.");
        return;
    }
    console.log("File content:", data);
});

console.log("Doing other things while we wait...");

In this scenario, "Doing other things" will print before the file content because Node.js doesn't wait for the file to finish loading.


The Problem: Callback Hell

Callbacks work great for one task. But what if you need to read a file, then use that data to query a database, then save that result to another file? You end up with code that looks like a sideways pyramid:

fs.readFile('user.json', (err, user) => {
    getDatabase(user.id, (err, db) => {
        updateRecord(db, (err, result) => {
            saveLog(result, (err) => {
                // It just keeps going...
            });
        });
    });
});

This is Callback Hell. It’s hard to read, nearly impossible to debug, and handles errors very poorly.


Promise-Based Handling

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. Think of it as a literal promise: "I don't have the data yet, but I promise to give it to you soon, or tell you if I failed."

A Promise is always in one of three states:

  1. Pending: Still working on it.

  2. Resolved (Fulfilled): Task finished successfully.

  3. Rejected: Something went wrong.


Benefits of Promises: Readability vs. Callbacks

Promises allow us to "chain" operations using .then() and handle all errors in one place with .catch().

Feature

Callbacks

Promises

Structure

Nested (Pyramid)

Linear (Chained)

Error Handling

Must handle in every single loop

One .catch() handles the whole chain

Readability

Poor for complex logic

High; reads like a list of steps


The same nested code from earlier, but with Promises:

readFilePromise('user.json')
    .then(user => getDatabase(user.id))
    .then(db => updateRecord(db))
    .then(result => saveLog(result))
    .catch(err => console.log("An error happened somewhere!"));

Conclusion

Asynchronous programming is what makes Node.js so fast and efficient. While callbacks were the original way to handle this, they quickly became messy as apps grew. Promises brought order to the chaos, giving us a cleaner, more reliable way to manage time-consuming tasks.