# Promises and Async Flow in JavaScript

> What Promises are in JavaScript, how to escape callback hell, and the essential patterns for chaining then/catch and managing async flow.

- Published: 2017-09-03
- Category: Languages
- Tags: JavaScript
- Reading time: 4 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/promises-and-async-flow-in-javascript/
- Language: en-US
- Author: Muhammet Şafak

---
Async operations in JavaScript have always felt a little strange. The language is single-threaded, yet [AJAX requests](/en/blog/sending-data-without-page-reload-jquery-ajax), timers, and file reads all happen without blocking. For years, `callback` functions were our primary tool for dealing with this. They work — but at some point they become completely unreadable.

**Promise** is an object introduced in [ES6](/en/blog/getting-started-with-es6-let-const-and-arrow-functions) that represents the future result of an async operation. It exists in one of three states: pending (not yet settled), fulfilled (completed successfully), or rejected (failed). This structure offers a far more readable and manageable alternative to nested callback chains.

## Callback hell

To see where the problem comes from, consider this scenario: fetch a user, then fetch that user's orders, then fetch the products in the first order.

```javascript
getUser(userId, function(user) {
    getOrders(user.id, function(orders) {
        getProducts(orders[0].id, function(products) {
            // Do something
        }, function(err) {
            console.error('Product error:', err);
        });
    }, function(err) {
        console.error('Order error:', err);
    });
}, function(err) {
    console.error('User error:', err);
});
```

Each step requires two callbacks: one for success, one for failure. The deeper the indentation grows, the harder the code becomes to follow. This pattern is known as "callback hell" or the "pyramid of doom."

## The same flow with Promises

```javascript
getUser(userId)
    .then(function(user) {
        return getOrders(user.id);
    })
    .then(function(orders) {
        return getProducts(orders[0].id);
    })
    .then(function(products) {
        // Do something with products
    })
    .catch(function(err) {
        console.error('An error occurred:', err);
    });
```

All errors are caught in a single `catch` block. If any step in the chain throws or rejects, execution jumps straight to `catch`. The code reads left-to-right, top-to-bottom.

## Creating a Promise

To write your own function that returns a Promise, use the `new Promise()` constructor:

```javascript
function delay(ms) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('Wait complete');
        }, ms);
    });
}

delay(1000).then(function(message) {
    console.log(message); // "Wait complete"
});
```

When `resolve` is called, the Promise fulfills and `.then()` fires. When `reject` is called — or an exception is thrown — `.catch()` fires.

## Parallel requests with Promise.all

When you need to kick off multiple async operations simultaneously and wait for all of them, use `Promise.all`:

```javascript
var userRequest = fetch('/api/user/1');
var settingsRequest = fetch('/api/settings');

Promise.all([userRequest, settingsRequest])
    .then(function(responses) {
        var userResponse = responses[0];
        var settingsResponse = responses[1];
        // Both are ready
    })
    .catch(function(err) {
        // If either one fails, we end up here
    });
```

Instead of waiting for them sequentially, both go out in parallel; the total time equals the duration of the slowest request.

`Promise.all` has one limitation: if any Promise in the array rejects, the entire group is considered failed and control goes to `catch`. If you want to let some operations fail while continuing with the rest, `Promise.allSettled` is the better fit — but that arrived in ES2020. Back in 2017 you had to handle that case by hand.

## A common pitfall

Forgetting `return` in a Promise chain is one of the most frequent mistakes. If you don't return a new Promise inside `.then()`, the rest of the chain continues without waiting for it:

```javascript
// Wrong: missing return
.then(function(user) {
    getOrders(user.id); // This Promise is never awaited
})

// Correct: return is present
.then(function(user) {
    return getOrders(user.id);
})
```

This mistake is easy to make and hard to spot while debugging. The symptom: the `orders` parameter arrives as `undefined` in the next `.then()` because the previous step returned nothing. Anyone reading the code for the first time usually spends a few minutes hunting for this.

## With ES6 arrow functions

Arrow functions make the chain noticeably more compact:

```javascript
getUser(userId)
    .then(user => getOrders(user.id))
    .then(orders => getProducts(orders[0].id))
    .then(products => console.log(products))
    .catch(err => console.error(err));
```

A single-expression arrow function has an implicit `return`, which also reduces the chance of the missing-return bug. Just remember: as soon as you add a block body `{}`, the `return` is required again.

Truly internalizing Promises takes a bit of time — especially building the right mental model for how chaining behaves. Coming from PHP, where synchronous code runs line by line, the idea that "this function isn't running right now, it will run later" requires a genuine paradigm shift. But once it clicks, you have no desire to go back to nested callback pyramids.

A Promise is not a value — it's a commitment that a value will arrive. Once that idea sinks in, it becomes clear why each `.then()` in a chain returns a new Promise. The value doesn't exist yet; you don't know when it will be ready, but you've already described what to do when it is. That habit of defining future work ahead of time eventually opens the door to a much broader way of thinking about programming. Back in 2017, fully grasping this took me some time — but once I did, my whole relationship with JavaScript changed.
