Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

Updated
3 min readView as Markdown

Imagine your server is a busy restaurant waiter. If he stops everything to wait for one customer's order from the kitchen, all other tables suffer. Node.js async programming lets him take more orders while the kitchen works in the background.

Why Node.js Needs Async Code

Node.js runs on a single thread. One slow task blocks everything—like readFileSync() waiting for a huge file.

Blocking Example:

const fs = require("fs");
const data = fs.readFileSync("bigfile.txt", "utf-8"); // App freezes here
console.log(data);
console.log("Done"); // Only runs AFTER file finishes

Non-blocking Magic:

const fs = require("fs");
console.log("Start");

fs.readFile("bigfile.txt", "utf-8", (err, data) => {
    console.log(data);
});

console.log("Done immediately!");
// Output: Start → Done immediately! → File content (later)

Node.js hands slow tasks to the OS and keeps running. Results come back via callbacks.

How Callbacks Work

A callback is a function you pass to another function: "Run me when you're done."

Simple Example:

function makeCoffee(name, callback) {
    console.log(`Making coffee for ${name}...`);
    callback("Coffee ready!");
}

function drinkCoffee(message) {
    console.log(message);
}

makeCoffee("Sharath", drinkCoffee);
// Hello Sharath
// Coffee ready!

Node.js Pattern: (error, result) => {}

fs.readFile("data.txt", "utf-8", (err, data) => {
    if (err) return console.error(err);
    console.log(data);
});

Key: Callback runs later, after Node.js handles other requests.

The Callback Hell Problem

Multiple dependent async calls create pyramid of doom:

fs.readFile("users.txt", (err, users) => {
    fs.readFile("orders.txt", (err, orders) => {
        fs.readFile("payments.txt", (err, payments) => {
            // Finally do something with all data
            console.log("All loaded!");
        });
    });
});

Problems:

  • Unreadable: Code shifts right like a bad novel

  • Error Hell: if (err) return; everywhere

  • Debug Nightmare: Stack traces make no sense

Promises: The Clean Fix

Promise = Future value container. Three states: pendingfulfilled/rejected.

Single File Example:

const fs = require("fs").promises;

fs.readFile("data.txt", "utf-8")
    .then(data => console.log(data))
    .catch(err => console.error(err));

Promise Chaining vs Callback Hell

Callback Hell:

fs.readFile("users.txt", (err, u) => {
    fs.readFile("orders.txt", (err, o) => {
        console.log(u, o);
    });
});

Promise Heaven:

const fs = require("fs").promises;

fs.readFile("users.txt", "utf-8")
    .then(users => {
        console.log(users);
        return fs.readFile("orders.txt", "utf-8");
    })
    .then(orders => console.log(orders))
    .catch(err => console.error(err));

Cleaner, flatter, one error handler!

Promise Superpowers

Feature Callback Promise
Readability Deep nesting Flat chain
Error Handling Copy-paste if(err) Single .catch()
Chaining Manual nesting .then() links
Debugging Confusing stacks Clear flow

The Complete Flow

1. Trigger async → Returns Promise immediately
2. Continue other work  
3. Promise settles → .then()/.catch() runs
4. Chain next async → Repeat

Real-World Example: API Chain

app.get('/user/:id', async (req, res) => {
    try {
        const user = await getUser(req.params.id);
        const orders = await getOrders(user.id);
        const total = await calculateTotal(orders);
        
        res.json({ user, orders, total });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

Async Evolution Summary

  1. Callbacks → Invented async (good start)

  2. Promises → Fixed callback hell (game changer)

  3. Async/Await → Promises + sugar syntax (today's standard)

1 views