Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

Updated
7 min readView as Markdown
N

I build clean and simple web experiences and learn something new every day.

The first time I started reading Node.js code… I kept seeing things execute in a weird order.

Like this:

console.log("Start");

setTimeout(() => {
  console.log("Inside timeout");
}, 2000);

console.log("End");

And the output was:

Start
End
Inside timeout

At first… this completely confused me.
Because I expected code to run: top to bottom.

But Node.js had other plans. And that’s when I discovered one of the MOST important concepts in backend development:

Asynchronous Code

Because Node.js is built around async behavior. And honestly… once this concept clicks…Node.js architecture starts making WAY more sense.


Why Async Code Even Exists

Let’s think about something simple. Suppose your app is reading a huge file.

Example:

movie.mp4

Now imagine Node.js stops EVERYTHING while waiting for that file.

What happens?
entire application freezes.

No other requests.
No responses.
Nothing.

That would be terrible for servers handling thousands of users.

So instead…

Node.js uses:

Asynchronous execution

Meaning: “Start task now… continue other work… finish later.”


Real-Life Analogy

Imagine ordering coffee.

You don’t stand inside kitchen watching coffee being made.

You:

  • place order

  • continue doing other things

  • collect coffee later

That’s basically async behavior.


Node.js and Non-Blocking Behavior

This is one reason Node.js became popular. Node.js avoids blocking operations whenever possible.

Instead of waiting… it keeps handling other tasks. That makes applications feel faster and more scalable.


File Reading Example

This example made async code click for me.

const fs = require("fs");

fs.readFile("demo.txt", "utf8", (err, data) => {
  console.log(data);
});

console.log("File reading started");

What Happens Here?

Node.js does NOT stop execution.

Instead:

Start file reading
      ↓
Continue running code
      ↓
File finishes later
      ↓
Callback executes

That’s asynchronous flow.


Why This Feels Strange Initially

Because beginners expect:

Line 1
Line 2
Line 3

strictly in order.

But async code introduces: delayed execution.
Some code executes later when task completes.


Callback-Based Async Execution

Initially… callbacks were the main way Node.js handled async behavior.

Example:

fs.readFile("demo.txt", "utf8", (err, data) => {
  console.log(data);
});

This function:

(err, data) => {
  console.log(data);
}

is the callback.

Node.js says: “Once file reading finishes… execute this function.”


Callback Execution Flow

Read File Request
        ↓
Node.js Continues Running
        ↓
File Finishes Loading
        ↓
Callback Executes

This pattern appears EVERYWHERE in Node.js.


Why Callbacks Were Useful

Callbacks solved a huge problem: waiting.

Instead of freezing application… Node.js could continue serving other tasks. That’s powerful for backend systems.


But Then Came The Problem…

As applications became larger… callbacks started becoming messy.

Very messy.


Nested Callbacks

Example:

loginUser(() => {
  getProfile(() => {
    getPosts(() => {
      getComments(() => {
        console.log("Everything loaded");
      });
    });
  });
});

And honestly… this quickly becomes painful to read.


Callback Hell

This structure became known as:

Callback Hell

Because code keeps drifting:

right
right
right

Creating giant pyramids.


Visual Representation

Callback
   ↓
Nested Callback
      ↓
Nested Callback
         ↓
Nested Callback

Hard to debug.
Hard to maintain.
Hard to scale.


Another Big Problem

Error handling became ugly too.

Example:

fs.readFile("demo.txt", "utf8", (err, data) => {
  if (err) {
    console.log(err);
  } else {
    console.log(data);
  }
});

Now imagine multiple nested operations. Error handling becomes chaotic very quickly.


This Is Why Promises Were Introduced

Promises solved readability problems. Instead of deeply nested callbacks… code became flatter and cleaner.

And honestly…
Promises made async JavaScript MUCH easier to manage.


What is a Promise?

A Promise is basically: an object representing future completion of an async task.

Sounds complex. But the idea is simple.


Real Mental Model

Promise says: “I don’t have result right now… but I WILL eventually.”

That’s it.


Promise States

A Promise can be:

Pending
Fulfilled
Rejected

Pending

Task still running.


Fulfilled

Task completed successfully.


Rejected

Task failed.


Simple Promise Example

const promise = new Promise((resolve, reject) => {
  resolve("Success");
});

Here:

resolve()

means: operation succeeded.

And:

reject()

means: operation failed.


Using .then()

Promises use:

.then()

for successful results.

Example:

promise.then((data) => {
  console.log(data);
});

Output:

Success

Handling Errors with .catch()

promise
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log(err);
  });

Cleaner error handling. Less nesting. Much more readable.


Reading Files with Promises

Modern Node.js supports promise-based file reading.

Example:

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

fs.readFile("demo.txt", "utf8")
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log(err);
  });

Why Promises Feel Better

Compare this mentally.


Callback Style

Callback inside callback

Promise Style

Step-by-step chain

Promises flatten async logic. That readability matters A LOT in real applications.


Promise Flow

Async Task Starts
        ↓
Promise Created
        ↓
Success → then()
Failure → catch()

Another Important Insight

Promises didn’t remove async behavior.

They improved: async code organization.

That distinction matters.


Why Async Code Is So Important in Node.js

Node.js servers constantly handle:

  • database queries

  • API requests

  • file operations

  • authentication

  • uploads

These tasks take time. Async behavior prevents server from freezing while waiting. That’s one reason Node.js scales so well.


Small But Powerful Realization

At first I thought async code was just “weird execution order.”

But later I realized: async code is what allows Node.js to handle many operations efficiently.

That’s much deeper.


Common Beginner Confusion

People often think: async means parallel threads.

Not exactly.

Node.js mainly uses: event-driven non-blocking architecture.

Which is different from traditional multithreading models.


Callback vs Promise Readability

This comparison changed everything for me.


Callbacks

Deep nesting
Harder error handling
Messy flow

Promises

Cleaner chaining
Better readability
Centralized error handling

Because developers needed cleaner async code. As applications grew larger… callback hell became a serious maintainability problem.

Promises solved a huge part of that.


Another Important Realization

Even modern:

async/await

is actually built ON TOP of Promises. So understanding Promises first is VERY important.


Assignment Practice

Try these yourself:

1. Use setTimeout()
2. Read file using callback
3. Create nested callbacks
4. Create simple Promise
5. Use .then() and .catch()
6. Compare readability carefully

That comparison is where understanding becomes real.


Final Understanding

At first, async code feels confusing because execution stops behaving “normally.”

But underneath everything…

the goal is simple: avoid blocking the application while waiting for slow tasks.
Callbacks were the first major solution. Promises improved the structure and readability of that solution.

And honestly…
once async thinking clicks…
Node.js starts feeling WAY more powerful.

More from this blog

C

codeXninjaDev

54 posts

I build clean and simple web experiences and learn something new every day.