Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks vs Promises

Updated
•5 min read•View as Markdown
Async Code in Node.js: Callbacks vs Promises

Node.js feels fast not because it does things instantly, but because it does not wait.

If you understand async code deeply, you unlock:

  • high-performance backend systems

  • scalable APIs

  • better debugging skills in interviews and real projects

This is not just theory — we will walk through how it actually works internally.


Why Asynchronous Code Exists in Node.js

Node.js runs on a single-threaded event loop. That means:

  • One main thread handles all requests

  • No blocking is allowed if you want scalability

If Node.js waited for slow operations like file I/O or API calls, the entire server would freeze.

So instead, Node.js delegates heavy work to the system and continues execution.


Event Loop Concept (Visual)

Image Image Image Image Image Image Image

Flow:

  1. Task starts

  2. Async operation goes to background (libuv / OS)

  3. Event loop continues executing other code

  4. Callback queue stores completed tasks

  5. Event loop picks and executes them


Real Scenario: File Reading (Blocking vs Non-Blocking)

Blocking Version

const fs = require('fs');

const data = fs.readFileSync('file.txt', 'utf-8');
console.log(data);
console.log("Next step");

Problem:

  • Execution stops until file is fully read

  • No concurrency


Non-Blocking Version (Callback)

const fs = require('fs');

fs.readFile('file.txt', 'utf-8', (err, data) => {
    if (err) throw err;
    console.log(data);
});

console.log("Next step");

Output order:

Next step
(file content later)

Callback Execution Flow (Step-by-Step)

Image Image Image Image Image Image
  1. readFile is called

  2. Node.js sends file task to system

  3. Main thread continues

  4. Once file is ready → callback pushed to queue

  5. Event loop executes callback

This is the foundation of Node.js scalability.


The Real Problem: Callback Hell

As systems grow, you rarely have just one async operation.

fs.readFile('file1.txt', 'utf-8', (err, data1) => {
    fs.readFile('file2.txt', 'utf-8', (err, data2) => {
        fs.readFile('file3.txt', 'utf-8', (err, data3) => {
            console.log(data1, data2, data3);
        });
    });
});

Callback Hell Visualization

Image Image Image Image Image Image

Problems:

  • Deep nesting structure

  • Hard to maintain

  • Error handling scattered

  • Code readability collapses

This is where modern JavaScript evolved.


Promises: Structured Async Handling

A Promise is an object that represents a future result.

It has three states:

  • Pending

  • Fulfilled

  • Rejected


Promise Lifecycle

Image Image Image Image Image Image Image

Promise-Based File Reading

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

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

console.log("Next step");

Solving Callback Hell with Promises

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

fs.readFile('file1.txt', 'utf-8')
    .then(data1 => {
        return fs.readFile('file2.txt', 'utf-8')
            .then(data2 => [data1, data2]);
    })
    .then(([data1, data2]) => {
        return fs.readFile('file3.txt', 'utf-8')
            .then(data3 => [data1, data2, data3]);
    })
    .then(([data1, data2, data3]) => {
        console.log(data1, data2, data3);
    })
    .catch(err => console.error(err));

Notice:

  • No pyramid structure

  • Flow is linear

  • Centralized error handling


Callback vs Promise (Technical Comparison)

Aspect Callback Promise
Execution style Nested Chained
Error handling Distributed Centralized
Readability Low High
Debugging Difficult Easier
Composition Weak Strong (Promise chaining)

Internal Difference (Important for Interviews)

Callback:

  • Passed directly to async function

  • No control over execution timing

  • Can be called multiple times (risk)

Promise:

  • Guarantees single resolution

  • Immutable state

  • Supports chaining and composition


Visual Comparison: Callback vs Promise Flow

Image Image Image Image Image Image Image

Advanced Insight: Microtask Queue vs Callback Queue

This is where most candidates fail in interviews.

Node.js has:

  • Callback Queue (macrotasks)

  • Microtask Queue (Promises)

Priority:

  1. Current execution

  2. Microtasks (Promises)

  3. Macrotasks (Callbacks)


Execution Order Example

console.log("Start");

setTimeout(() => console.log("Callback"), 0);

Promise.resolve().then(() => console.log("Promise"));

console.log("End");

Output:

Start
End
Promise
Callback

Microtask vs Macrotask Visualization

Image Image Image Image Image

Final Step: async/await (Modern Standard)

Promises become even cleaner with async/await.

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

async function readFiles() {
    try {
        const data1 = await fs.readFile('file1.txt', 'utf-8');
        const data2 = await fs.readFile('file2.txt', 'utf-8');
        const data3 = await fs.readFile('file3.txt', 'utf-8');

        console.log(data1, data2, data3);
    } catch (err) {
        console.error(err);
    }
}

This looks synchronous, but it is fully asynchronous under the hood.


Key Takeaways

  • Node.js uses async code to avoid blocking the event loop

  • Callbacks are the foundation but lead to complexity

  • Promises provide structure and reliability

  • Microtasks (Promises) execute before callbacks

  • async/await is built on top of Promises and is the current standard

More from this blog