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:
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:
-
AsyncStorage.getItem()
is fetching the value associated tokey
, which is a stringified JSON. (The data stored has this shape:{ items: [{ date, ... }, { ... }, ... ]}
) - If the query doesn't return
null
, we parse the JSON and return it as an array. Otherwise we return an empty array. - We filter the returned array and keep only the items whose
date
is greater than or equal to now. - We create an object and assign the filtered array to its
items
property. - We stringify the object.
- 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.
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.
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:
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. 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
1 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.
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 seeError: "Something went wrong"
, which is the error we defined in the function's body. - If
asyncFunction
returnsnull
, the promise will befulfilled
, but the nextthen
cannot iterate over it, so it will reject and throw an error. This error will be caught by ourcatch
and we'll see a warning sayingTypeError: "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:
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
returnsnull
, we will still see the messageTypeError: "data is null"
, but this time it will be logged as an error instead of a warning, because it fired after thecatch
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
returnsundefined
(because we haven't told it to return anything else) as the value of afulfilled
promise.Since the previous promise is
fulfilled
,then
tries to execute itsonResolve
callback using the data returned. Since this data isundefined
, 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.
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:
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.
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 nextthen(onResolve)
. Thisthen
will return a new promise that will either befulfilled
orrejected
.A
rejected
promise will jump straight to the nextcatch
orthen(..., onReject)
statement. In turn, it will return a new promise. Unless the code incatch
causes it to reject, the newly returned promise will allow anythen
statements below it to be executed normally.
1: 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.
Top comments (0)