Promises
In my previous post, How I Learned to Stop Worrying and Embrace fetch(), I discussed the basics of how fetch() retrieves data and then uses that data to communicate with our API and render it to our web application. But that was a discussion of the surface level effects of working in an asynchronous manner. Let’s dig under the hood a little bit.
Imagine that we want to build an app using the Pokemon API and we decide to use fetch() to get the data from the source URL. We’ll start simple.
pokeApi = "https://pokeapi.co/api/v2/pokemon?limit=150";fetch(pokeApi);
What would our fetch() return?
It returns a Promise: an object that represents either the success or failure of an asynchronous operation (like fetch()). Once a Promise object is returned, you can attach callbacks to it, instead of passing those callbacks into a function.
Above, we can see that our Promise’s state is listed as “fulfilled”. This means that it was a success! There are two additional states: “pending” and “rejected”. If a Promise is pending, it is in its initial state and has not yet been fulfilled or rejected. If a Promise is rejected, then the operation failed.
This is important to understand when using fetch(), because typically we will want to chain a .then() method onto the Promise we’ve had returned. Our then() will only complete its operations if the Promise has been fulfilled. It won’t work on a rejection.
Let’s add our .then() to the previous example:
Since our initial fetch() returned a fulfilled Promise, our then() works properly — and because of that, our console logs our response.
We could also add a .catch() method at the end of our chain, which would act a lot like then(), but would only be called if the returned Promise was rejected. This is a great way to signal that there has been an error.
fetch(pokeApi)
.then(response => response.json())
.then(pokemon => {
pokemon.results.forEach(p => {
console.log(p.name)
})
})
.catch((error) => {
console.error(error.message);
})
To break it down, in this example:
- We call fetch and pass in our API URL and wait for our Promise to be returned.
- If the Promise is fulfilled, the first then() method is run, taking the response and transforming it into a JSON object.
- If the Promise for that then() is fulfilled, then the next then() is run, iterating over all of the Pokemon and logging their names to the console.
- If the Promise is rejected at any point, then catch() will display an error message in the console.
This demonstrates one of the benefits of Promises (those returned by fetch(), as well as those written by the user): they can be chained. You can attach multiple callbacks to accomplish your goals and they will be run in the order in which they were inserted. Multiple asynchronous functions can be run, one after the other, using the data returned from the previous step.
It should be noted when using fetch() or a Promise in any other context, that any attached then() will not be executed until the JavaScript is finished running in the previous event loop and a response of fulfilled is returned. Because the code is asynchronous, this can lead to some interesting behavior.
function fetchPokemon(){
fetch(pokeApi)
.then(response => response.json())
.then(pokemon => {
pokemon.results.forEach(p => {
console.log(p.name)
})
})
.catch((error) => {
console.error(error.message);
});
catchEmAll();
}function catchEmAll(){
console.log("Gotta Catch 'Em All!")
}
Here, we’ve added our established fetch() code to a function called fetchPokemon(). I’ve also created a separate function called catchEmAll() that simply logs the famous catchphrase to the console. I call catchEmAll() outside of the fetch chain.
What would you expect to see if you run the fetchPokemon() function?
Originally, I believed that we would see our list of logged Pokemon names printed in the console, followed by “Gotta Catch ’Em All!”. Visually, that appears to be what is happening in the code on a basic level. But that isn’t actually the case:
Why did this happen?
Because our fetch() didn’t receive the Promise immediately. As it waited for it to be fulfilled, JavaScript ran our catchEmAll() function, returning that data, and only then was able to proceed to complete the rest of the callbacks after fetch(). This is an essential consideration when determining the order of operations for your code.
Understanding Promises and how they are fulfilled or rejected will help you to get the best possible use out of fetch() and provide a new tool for manipulating data in JavaScript.