Promise flow: An in-depth look at then and catch
Tags: javascript, promises, asynchronous
Published: 2019-11-26 13:54:42

Promises are one way in which you can handle asynchronous operations in JavaScript. Today we are going to look at how the promise methods `then` and `catch` behave and how the information flows from one another in a chain.

I think one of the strengths of promise syntax is that it is very intuitive. This is a slightly modified version of a function I wrote to retrieve, modify and re-store information using [React Native's community Async Storage](https://github.com/react-native-community/async-storage):

```javascript
const findAndRemoveOutdated = (key) => AsyncStorage.getItem(key)
    .then(data => data != null ? JSON.parse(data).items : [])
    .then(items => items.filter(x => new Date(x.date) >= Date.now()))
    .then(items => ({ items }))
    .then(JSON.stringify)
    .then(items => AsyncStorage.setItem(key, items))
```

Even if you don't know how Async Storage works, it's reasonably easy to see how the data flows from one `then` to the next one. Here's what's happening:

1. `AsyncStorage.getItem()` is fetching the value associated to `key`, which is a stringified JSON. (The data stored has this shape: `{ items: [{ date, ... }, { ... }, ... ]}`)
2. If the query doesn't return `null`, we parse the JSON and return it as an array. Otherwise we return an empty array.
3. We filter the returned array and keep only the items whose `date` is greater than or equal to now.
4. We create an object and assign the filtered array to its `items` property.
5. We stringify the object.
6. We save the new object in place of the old one.

So it is pretty intuitive. It reads like a list of steps manage the data, which it's what it is really. But while a bunch of `then`s is relatively easy to follow, it might get a bit more complicated when `catch` is involved, especially if said `catch` isn't at the end of the chain.

## An example of promise

For the rest of the article, we are going to work with an asynchronous function that simulates a call to an API. Said API fetches ninja students and sends their id, name and grade (we will set an object with a few students to use). If there are no students found, it sends `null`. Also, it's not a very reliable API, it fails around 15% of the time.

```javascript
const dataToReturn = [{ //Our ninja students are stored here.
  id: 1,
  name: 'John Spencer',
  grade: 6,
},{
  id: 2,
  name: 'Tanaka Ike',
  grade: 9,
},{
  id: 3,
  name: 'Ha Jihye',
  grade: 10,
}]

const asyncFunction = () => new Promise((resolve, reject) => {
  setTimeout(() => {
    const random = Math.random()
    return random > 0.4 //Simulates different possible responses
            ? resolve(dataToReturn) //Returns array
            : random > 0.15
            ? resolve(null) //Returns null
            : reject(new Error('Something went wrong')) //Throws error
  }, Math.random() * 600 + 400)
})
```

If you want to get a hang of what it does, just copy it and run it a few times. Most often it should return `dataToReturn`, some other times it should return `null` and on a few occasions it should throw an error. Ideally, the API's we work in real life should be less error prone, but this'll be useful for our analysis.

## The basic stuff

Now we can simply chain `then` and `catch` to do something with the result.

```javascript
asyncFunction()
    .then(console.log)
    .catch(console.warn)
```

Easy peasy. We retrieve data and log it into the console. If the promise rejects, we log the error as a warning instead. Because `then` can accept two parameters (`onResolve` and `onReject`), we could also write the following with the same result:

```javascript
asyncFunction()
    .then(console.log, console.warn)
```

## Promise state and `then`/`catch` statements

I wrote in a previous article that [a promise will have one of three different states](https://dev.to/savagepixie/promises-in-javascript-3j60#how-to-create-a-promise). It can be `pending` if it is still waiting to be resolved, it can be `fulfilled` if it has resolved correctly or it can be `rejected` if something has gone wrong.

When a promise is `fulfilled`, the program goes onto the next `then` and passes the returned value as an argument for `onResolve`. Then `then` calls its callback and returns a new promise that will also take one of the three possible states.

When a promise is `rejected`, on the other hand, it'll skip to the next `catch` or will be passed to the `then` with the `onReject` parameter and pass the returned value as the callback's argument. So all the operations defined between the rejected promise and the next `catch`<sup>[1](#myfootnote1)</sup> will be skipped.

### A closer look at `catch`

As mentioned above, `catch` catches any error that may occur in the execution of the code above it. So it can control more than one statement. If we were to use our `asyncFunction` to execute the following, we could see three different things in our console.

```javascript
asyncFunction()
    //We only want students whose grade is 7 or above
    .then(data => data.filter(x => x.grade >= 7))
    .then(console.log)
    .catch(console.warn)
```

- If everything goes all right, we will see the following array:
  ```
{
  id: 2,
  name: 'Tanaka Ike',
  grade: 9,
},{
  id: 3,
  name: 'Ha Jihye',
  grade: 10,
}
```
- If `asyncFunction` rejects and throws an error, we'll see `Error: "Something went wrong"`, which is the error we defined in the function's body.
- If `asyncFunction` returns `null`, the promise will be `fulfilled`, but the next `then` cannot iterate over it, so it will reject and throw an error. This error will be caught by our `catch` and we'll see a warning saying `TypeError: "data is null"`.

But there's more to it. Once it has dealt with the rejection, `catch` returns a new promise with the state of `fulfilled`. So if we were to write another `then` statement after the `catch`, the `then` statement would execute after the `catch`. So, if we were to change our code to the following:

```javascript
asyncFunction()
    //We want to deal with the error first
    .catch(console.warn)
    //We still only want students whose grade is 7 or above
    .then(data => data.filter(x => x.grade >= 7))
    .then(console.log)
```

Then we could still see three different things in our console, but two would be slightly different:
- If `asyncFunction` returns `null`, we will still see the message `TypeError: "data is null"`, but this time it will be logged as an error instead of a warning, because it fired after the `catch` statement and there was nothing else to control it.
- If `asyncFunction` returns an error, `catch` will still handle it and log it as a warning, but right below it we'll see an error: `TypeError: "data is undefined"`. This happens because after it deals with the error, `catch` returns `undefined` (because we haven't told it to return anything else) as the value of a `fulfilled` promise.

    Since the previous promise is `fulfilled`, `then` tries to execute its `onResolve` callback using the data returned. Since this data is `undefined`, it cannot iterate over it with filter and throws a new error, which isn't handled anywhere.

Let's now try to make our `catch` return something. If `asyncFunction` fails, we'll use an empty array instead.

```javascript
asyncFunction()
    .catch(error => {
      console.warn(error)
      return []
    })
    .then(data => data.filter(x => x.grade >= 7))
    .then(console.log)
```

Now, if the call to `asyncFunction` rejects, we will still see the warning in our console, but it'll be followed by an empty array instead of a type error. The empty array that it returns becomes the `data` that the following `then` filters. Since it is an array, the `filter` method works and returns something.

We still have the possible error if `asyncFunction` returns `null`, though. So let's deal with it:

```javascript
asyncFunction()
    .catch(error => {
      console.warn(error)
      return []
    })
    .then(data => data.filter(x => x.grade >= 7))
    .catch(error => {
      console.warn(error)
      return []
    })
    .then(console.log)
```

We've just copied the same `catch` statement and pasted it after the filtering `then`. Now, if an error occurs on either promise, we will see it logged as a warning (either as a type error or as our custom error) and an empty array logged under it. That is because our `catch` statements have dealt with all errors and returned `fulfilled` promises, so the `then` chain continues until it's time to log it in the console.

In fact, while we're at it, we might realise that the first `catch` is superfluous. It's doing the exact same thing as the second one and the result of filtering an empty array is always an empty array, so it doesn't really matter if the empty array returned by it gets filtered or not. So we can just dispose of it.

```javascript
asyncFunction()
    .then(data => data.filter(x => x.grade >= 7))
    .catch(error => {
      console.warn(error)
      return []
    })
    .then(console.log)
```

If we wanted, instead we could do some different error handling. We could feed it fake data (not advisable in real production), try fetching data from another API, or whatever our system requires.

## Conclusion

Whenever a promise is resolved, the runtime will execute the following `then` and `catch` statements depending on the promise's state.

- A `fulfilled` promise will trigger the next `then(onResolve)`. This `then` will return a new promise that will either be `fulfilled` or `rejected`.

- A `rejected` promise will jump straight to the next `catch` or `then(..., onReject)` statement. In turn, it will return a new promise. Unless the code in `catch` causes it to reject, the newly returned promise will allow any `then` statements below it to be executed normally.

---

<a name="myfootnote1">1</a>: From now on, I will only refer to `catch` as a method to handle errors, because it is more common. Know that anything that I say about `catch` also works for `then` when an `onReject` callback is passed to it.