# Async Code in Node.js: Callbacks and Promises

When you first start working with Node.js, one concept keeps showing up everywhere: **asynchronous code**.

At first glance, it feels confusing — why not just run code line by line like normal programs?

Let’s break it down step by step.

## Why Async Code Exists in Node.js

Node.js is built on a **single-threaded, non-blocking event loop architecture**.

This means:

*   It can handle many operations at once
    
*   But it does **not** create a new thread for each task
    

Now imagine this scenario:

```javascript
const data = fs.readFileSync("file.txt");
console.log(data);
console.log("Done");
```

This is **blocking (synchronous)**:

*   Node waits for the file to be read
    
*   Nothing else happens during that time
    

### Problem

If file reading takes time:

*   Your server is stuck
    
*   Other users have to wait
    

### Solution: Asynchronous Code

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

console.log("Done");
```

Now:

*   Node **does not wait**
    
*   It continues execution
    
*   Handles the result later
    

## Real Scenario: File Reading

Let’s understand the flow properly.

### Code:

```javascript
console.log("Start");

fs.readFile("file.txt", "utf-8", (err, data) => {
  if (err) {
    console.log("Error:", err);
    return;
  }
  console.log("File Data:", data);
});

console.log("End");
```

### Step-by-Step Execution

1.  `"Start"` is printed
    
2.  `fs.readFile` is sent to the system (async task)
    
3.  Node **does NOT wait**
    
4.  `"End"` is printed
    
5.  Once file reading completes → callback executes
    
6.  `"File Data"` is printed
    

### Output Order

```javascript
Start
End
File Data: ...
```

This is the **core idea of async in Node.js**.

## Callback-Based Async Execution

A **callback** is simply:

> A function passed as an argument to another function, executed later.

### Example:

```javascript
function fetchData(callback) {
  setTimeout(() => {
    callback("Data received");
  }, 1000);
}

fetchData((data) => {
  console.log(data);
});
```

### Key Idea

*   You give control to another function
    
*   It calls you back when work is done
    

## Problem: Callback Hell

Now let’s chain multiple async operations:

```javascript
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);
    });
  });
});
```

### Issues

*   Deep nesting (pyramid shape)
    
*   Hard to read
    
*   Hard to debug
    
*   Error handling becomes messy
    

This is called:

> **Callback Hell** (or Pyramid of Doom)

## Promise-Based Async Handling

Promises were introduced to fix these problems.

### What is a Promise?

A Promise represents:

> A value that will be available **in the future**

### States of a Promise

*   **Pending**
    
*   **Resolved (fulfilled)**
    
*   **Rejected**
    

### Basic Example

```javascript
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data received");
  }, 1000);
});

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

## Rewriting File Example Using Promises

Using `fs.promises`:

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

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

### Better Version (Flat Chain)

```javascript
fs.readFile("file1.txt", "utf-8")
  .then((data1) => {
    console.log(data1);
    return fs.readFile("file2.txt", "utf-8");
  })
  .then((data2) => {
    console.log(data2);
    return fs.readFile("file3.txt", "utf-8");
  })
  .then((data3) => {
    console.log(data3);
  })
  .catch((err) => {
    console.log(err);
  });
```

## Benefits of Promises

### 1\. Better Readability

*   No deep nesting
    
*   Linear flow
    

### 2\. Centralised Error Handling

```js
.catch((err) => {
  console.log(err);
});
```

Instead of handling errors at every level.

### 3\. Easier Composition

You can chain multiple async operations cleanly.

### 4\. Foundation for Async/Await

Promises enable:

```javascript
const data = await fs.readFile("file.txt", "utf-8");
```

## Callback vs Promise Comparison

| Feature | Callbacks | Promises |
| --- | --- | --- |
| Readability | Poor (nested) | Clean (chainable) |
| Error Handling | Scattered | Centralized (`.catch`) |
| Debugging | Difficult | Easier |
| Scalability | Poor | Better |
| Structure | Pyramid-like | Linear flow |

![](https://cdn.hashnode.com/uploads/covers/69517129b3f42beb72514f64/a03a6410-44a7-4232-8009-6fb96670eb6e.png align="center")

## Final Insight

*   Callbacks are **low-level primitives**
    
*   Promises are **structured abstractions**
    

In modern Node.js:

> You should prefer **Promises (and async/await)** over callbacks
