# Callbacks and Promises

In our previous article we have seen that why we need async code. In this we will go through how to write async code

## Callbacks

A callback is the simplest possible async mechanism: pass a function as an argument, and Node.js will call it when the async work is done.

Here's the pattern, using `fs.readFile` as the canonical example

```javascript
const fs = require('fs');

fs.readFile('./config.json', 'utf8', function(err, data) {
  if (err) {
    console.error('Failed to read file:', err);
    return;
  }
  console.log(data);
});

console.log('This runs immediately — before the file is read');
```

Notice two things about this pattern:

1.  Error-first convention. Node.js callbacks always receive the error as the first argument. If there's no error, it's null. If there is an error, the data argument will be undefined. This is called the "error-first" or "Node.js callback style." It's a convention, not enforced by the language but almost every Node.js built-in and npm package follows it. Always check err before using data.
    
2.  Execution order. The console.log at the bottom runs before the file contents print. The readFile call registers the callback and returns immediately. The callback fires later, when the OS finishes reading. This is the async reordering you saw in the event loop article and it's always the behavior with callbacks.
    

## Callbacks in Practice: A Real Scenario

Say you need to:

1.  Read a user's config file
    
2.  Parse it as JSON
    
3.  Use the `userId` from the config to fetch user data from a database
    
4.  Log the user's name
    

```javascript
const fs = require('fs');

fs.readFile('./config.json', 'utf8', function(err, data) {
  if (err) {
    console.error('Could not read config:', err);
    return;
  }

  let config;
  try {
    config = JSON.parse(data);
  } catch (parseErr) {
    console.error('Config is not valid JSON:', parseErr);
    return;
  }

  db.findUser(config.userId, function(err, user) {
    if (err) {
      console.error('Could not fetch user:', err);
      return;
    }

    console.log('User:', user.name);
  });
});
```

This works. It's perfectly valid Node.js code. But notice what happened to the structure: every async step requires nesting inside the previous one's callback. And we only have three operations. Real applications often have five, eight, ten.

### Callback Hell

The nesting problem has a name in the Node.js community: **callback hell** — sometimes called the "pyramid of doom" because of the shape the indentation makes.

Here's what five sequential async steps looks like with callbacks:

```javascript
readFile(path, function(err, data) {
  if (err) return handleError(err);
  parseConfig(data, function(err, config) {
    if (err) return handleError(err);
    fetchUser(config.userId, function(err, user) {
      if (err) return handleError(err);
      fetchPermissions(user.id, function(err, perms) {
        if (err) return handleError(err);
        generateReport(user, perms, function(err, report) {
          if (err) return handleError(err);
          console.log(report);
        });
      });
    });
  });
});
```

This is technically correct but practically painful:

*   Every level of nesting adds cognitive load
    
*   Error handling must be repeated at every step
    
*   You can't reuse a step independently — everything is tangled together
    
*   Adding a sixth step means another level of indentation
    
*   Debugging means mentally tracking which variables are in scope at which nesting depth
    

The code describes what happens in the right order, but it doesn't *read* that way. You can't scan it top-to-bottom and understand the flow linearly.

You can partially escape callback hell without Promises by naming your callbacks and defining them separately instead of inline. The same five-step sequence above can be written flat if you define each callback as a named function. It's not as ergonomic as Promises, but it's far more readable than nested inline functions. Many legacy Node.js codebases use exactly this pattern.

# Promises

A Promise is an object that represents the *eventual result* of an async operation. Instead of passing a callback *into* a function, the function returns a Promise object that you work with.

The Promise can be in one of three states:

*   **Pending** — the operation is still in progress
    
*   **Fulfilled** — the operation succeeded; the result is available
    
*   **Rejected** — the operation failed; an error is available
    

Once a Promise settles (fulfills or rejects), it stays that way forever. It won't randomly fulfill again, or change its result. This immutability is one of the things that makes Promises easier to reason about than callbacks.

Here's the same file-read example with a Promise-based API:

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

fs.readFile('./config.json', 'utf8')
  .then(function(data) {
    console.log(data);
  })
  .catch(function(err) {
    console.error('Failed to read file:', err);
  });
```

The `.then()` runs when the Promise fulfills. The `.catch()` runs if it rejects. And critically you can chain them.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/bf228496-d164-4ebb-86d9-39519969dac0.png align="center")

### Chaining

Here's the power move. Instead of nesting, you chain `.then()` calls in a flat sequence:

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

fs.readFile('./config.json', 'utf8')
  .then(data => JSON.parse(data))           // step 2: parse
  .then(config => db.findUser(config.userId)) // step 3: fetch user
  .then(user => {
    console.log('User:', user.name);
  })
  .catch(err => {
    console.error('Something went wrong:', err); // catches errors from ANY step
  });
```

Compare this to the callback version. It's the same five operations, written flat, with a single `.catch()` at the end that handles any error from any step.

**This is why Promises were adopted so rapidly.** The logic flows top to bottom. You can read it and understand the sequence. Error handling is centralized. Each `.then()` receives the return value of the previous one so passing data from step to step is just a matter of returning it.

## Creating Your Own Promises

You won't just consume Promises — you'll need to wrap older callback-based code in them. Here's how:

```javascript
function readFileAsync(path) {
  return new Promise(function(resolve, reject) {
    fs.readFile(path, 'utf8', function(err, data) {
      if (err) {
        reject(err);    // → Promise rejects with the error
      } else {
        resolve(data);  // → Promise fulfills with the data
      }
    });
  });
}

// Now you can use it with .then()/.catch()
readFileAsync('./data.txt')
  .then(data => console.log(data))
  .catch(err => console.error(err));
```

The `new Promise(executor)` pattern: inside the executor function, you call `resolve(value)` when the work succeeds and `reject(error)` when it fails. The Promise object carries that result forward.

**What most tutorials skip:** Node.js ships with `util.promisify()`, a built-in utility that automatically wraps any error-first callback function into a Promise-returning function. Instead of writing the wrapper above manually:

```javascript
const { promisify } = require('util');
const readFileAsync = promisify(fs.readFile);

readFileAsync('./data.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));
```

One line. Works on any standard Node.js callback-style function. And `fs.promises` (used earlier) is essentially the entire `fs` module pre-promisified — it's been available since Node 10 and is the modern default.

## Promise.all: Running Multiple Async Operations in Parallel

This is one of the most useful patterns in real applications, and it has no clean equivalent in callback code.

Instead of chaining operations one after another, `Promise.all` runs multiple async operations simultaneously and waits for all of them:

```javascript
const userPromise = db.findUser(userId);
const permsPromise = db.fetchPermissions(userId);
const prefsPromise = db.fetchPreferences(userId);

Promise.all([userPromise, permsPromise, prefsPromise])
  .then(([user, perms, prefs]) => {
    console.log(user.name, perms, prefs);
  })
  .catch(err => {
    // If ANY of the three fails, this runs
    console.error(err);
  });
```

Three DB queries running at the same time. All three results available together when they all finish. If any one fails, the `.catch()` fires.

Without Promises, achieving this with callbacks requires a manual counter or an async utility library. With Promises, it's one line.

Callbacks were the original Node.js async mechanism — simple, flexible, but painful to chain and error-handle at scale. They gave us "callback hell."

Promises replaced them with an object that represents a future value. `.then()` chains are flat and readable. `.catch()` handles errors from any step in the chain. `Promise.all` enables true parallelism in application code.

Understanding both is non-negotiable. You'll encounter callback-based APIs in older packages and existing codebases. You'll write and consume Promises in any modern Node.js project. And once you've internalized Promises, the final piece — `async/await` — clicks into place immediately.
