- Understand what a promise is and why it's beneficial.
- Know how to use a promise.
- Know how to chain promises.
- Be able to handle errors with our promises.
- Understand how the Fetch API is used to make HTTP requests
- Make HTTP requests using the Fetch API
- Promises
- then
- catch
- Pending
- Fulfilled
- Rejected
- Settled
- Callback Hell
- Chaining
- Success and Failure
- Fetch
A Promise is an object representing the eventual completion or failure of an asynchronous operation. This is most often seen when we make API calls. Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.
Using promises allows us to wait for certain code to finish execution prior to running the next bit of code. But why do we need that?
Pretend that you had a website that loads data from an API call, and then process that data to display for the user. If we tried to process the data before actually getting any data back we'd end up with a blank website or an error. With with promises we can ensure synchronicity.
Promises have three states:
- Fulfilled - The action relating to the promise succeeded.
- Rejected - The action relating to the promise failed.
- Pending - Hasn't fulfilled or rejected yet.
After a promise has been fulfilled or rejected, the promise is considered Settled.
Before promises we used callbacks. This would quickly become unwieldy if we were doing several asynchronous operations in a row.
This is callback hell:
doSomething(result => {
doSomethingElse(result, newResult => {
doThirdThing(newResult, finalResult => {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
With modern functions, we attach our callbacks to the returned promises instead. Promises return a promise. Adding a .then()
creates a promise chain:
doSomething().then(result => {
return doSomethingElse(result);
})
.then(newResult => {
return doThirdThing(newResult);
})
.then(finalResult => {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
The .then
takes in two optional arguments. The first argument is a callback function that will be fired upon the promise succeeding, the second argument is a callback function that will be fired upon the promise failing.
The callback to then
itself takes in an argument (called result
in the code bock); the result
's value is whatever the previous promise returned. If you are going to chain together promises with .then, your callbacks will all probably need to return something to the next .then(). The result of the original promise is not in scope of the second, third, fourth then()
The arguments to then
are optional and catch(failureCallback)
is short for then(null, failureCallback)
. catch
is the way we typically deal with error handling. You'll most often see a promise chain with multiple success calls and then just the one catch
at the end.
Remember each then
returns a promise.
Although you will most likely already be dealing with asynchronous calls that return promises, it is possible to create your own promise with its constructor function. To do this you write new Promise and then pass in a callback function. That callback function will take in two arguments (each of which will be a function), one for resolve, and one for reject. We call resolve(...) when what we were doing asynchronously was successful, and reject(...) when it failed. This resolve and reject function are both generated by the constructor generates a new Promise
. The names don't have to be resolve or reject but for clarity are often named as such.
Here's an example of what that might look like:
const promise = new Promise((resolve, reject) => {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
Run this code in your console and see how promises are chained:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 2000);
}).then((result) => {
alert(result);
return result + 2;
}).then((result) => {
alert(result);
return result + 2;
}).then((result) => {
alert(result);
return result + 2;
});
Now try running this code that intentionally throws an error. See how catch works?
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 2000);
}).then((result) => {
alert(result);
return result + 2;
}).then((result) => {
throw new Error('FAILED HERE');
alert(result);
return result + 2;
}).then((result) => {
alert(result);
return result + 2;
}).catch((e) => {
alert('error: ' + e)
});
The Fetch API provides an interface for fetching resources (including across the network). It will seem familiar to anyone who has used XMLHttpRequest, but the new API provides a more powerful and flexible feature set.
The fetch() method takes one mandatory argument, the path to the resource you want to fetch. It returns a Promise that resolves to the Response to that request, whether it is successful or not. You can also optionally pass in an options object as the second argument. An options object can contain any custom settings that you want to apply to the request such as the method (GET, POST, DELETE, etc), headers, body and many more.
Let's try this out!
We'll use the https://restcountries.eu
resource to load a list of countries based off of the user's search query. First, let's put together the html that we'll be using:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Countries</title>
<script src="index.js" defer></script>
</head>
<body>
<h1>Countries</h1>
<form>
<input
type="text"
placeholder="Enter a country name"
id="country-name-input"
required
/>
</form>
<section id="country-container"></section>
</body>
</html>
Now, we'll need to add our event listeners in our index.js
:
document.querySelector("form").addEventListener("submit", loadCountries);
function loadCountries(e) {
e.preventDefault();
const searchTerm = document.querySelector("#country-name-input").value;
fetch("https://restcountries.eu/rest/v2/name/" + searchTerm)
.then((response) => {
return response.json();
})
.then((countries) => {
countries.forEach((country) => {
console.log(country);
});
})
.catch((error) => {
console.log(error);
});
}
We are using fetch
, which returns a Promise that we that we can immediately attach a .then
to to process the response once the promise is fulfilled. For now, we're just printing in the console the countries that come back to us. Test it out with different search terms and make sure that you're getting back data.
What happens when you search for the number 23?
You'll notice that an error is thrown. The error says that countries.forEach is not a function
. This is a little bit unexpected and undesired. I'd like for you to add a debugger
on the line before return response.json()
and one before the forEach
then try again. While you're paused on the first debugger take a look at the Response object. Some things you may notice is that the Response object has the properties status
and ok
. Notice that the status is 404 (Not Found) and ok is false
. Now play through the debugger. Now our code is frozen on the line before the forEach
. This is undesired. We had a 404 status code but we still fulfilled our promise and moved on to the next chain. This is a NEGATIVE of fetch
. 4xx and 5xx status codes will NOT throw errors with fetch
. Instead we need to alter our fetch code like so:
fetch("https://restcountries.eu/rest/v2/name/" + searchTerm)
.then((response) => {
if(!response.ok) {
throw Error(`Something went wrong, status ${response.status}`);
}
return response.json();
})
.then((countries) => {
countries.forEach((country) => {
console.log(country);
});
})
.catch((error) => {
console.log(error);
});
Try running again and playing with the debuggers. You should see that we no longer make it to our second then
and instead jump right to our catch statement.
Once we've successfully grabbed our data we can use our DOM manipulation skills to add the list of countries to our webpage.
To handle other requests is simple. All you need to do is pass in an object with some data as the second argument to fetch. You could do something like this:
const button = document.querySelector("button");
button.addEventListener("click", fireRequest);
function fireRequest() {
const data = { name: 'My Name' };
const fetchData = {
method: 'POST',
body: data,
headers: new Headers(),
};
fetch("https://jsonplaceholder.typicode.com/posts", fetchData)
.then(response => {
if(!response.ok) {
throw Error(`Something went wrong, status ${response.status}`);
}
return response.json();
})
.then(response => {
console.log(response)
})
.catch(err => {
console.log(err)
})
}
Although it's relatively simple to find great free API's to make GET requests, most public API's won't allow you to change data on a database level. Thus there's not as many great places to practice making POST, PUT, PATCH, and DELETE requests. Because of this fact, let's go ahead and get our own backend running so that we can play around with any all request types. Follow the instructions and get it running.
Challenge 1: Find the total of cars that user 1 has, Add that to the total of cars that user 2 has. Then find the car with the id that matches that sum.
Challenge 2: Create a form that takes in a username and on submission adds a new user to the database.
Challenge 3: Create a show users button that prints a list of all the users.
Challenge 4: When you click on a user, remove them from the list and also delete them from the database.
- Fetch - MDN
- Fetch Parameters
- How to use the Fetch API
- Fetch vs Axios
- JavaScript: Learn Promises
- JavaScripot Promises: an Introduction
- MDN * Promise.all: Accepts an array of promises, and creates a single promise that only gets fulfilled if every promise in the array is fulfilled.
- A polyfill is required for consistent functionality across older browsers.