We're going to learn how to make HTTP requests in JavaScript. This is made possible by the fetch
function, which uses something called "promises" to manage async code. Here's a really quick example of what it looks like before we dive in:
fetch("https://pokeapi.co/api/v2/pokemon/pikachu").then((response) => {
console.log(response);
});
Before we look at promises, lets make sure we understand what problem they solve.
JavaScript is a single-threaded language. This means things generally happen one at a time, in the order you wrote the code.
console.log(1);
console.log(2);
console.log(3);
// logs 1, then 2, then 3
When something needs to happen out of this order, we call it asynchronous. JavaScript handles this using a "queue". Anything asynchronous gets pushed out of the main running order and into the queue. Once JS finishes what it was doing it moves on to the first thing in the queue.
console.log(1);
setTimeout(() => console.log(2), 1000);
console.log(3);
// logs 1, then 3, then (after 1 second) logs 2
It's intuitive that the above example logs 2
last, because JS has to wait a whole second before running the function passed to setTimeout
.
What's less intuitive is that this is the same even with a timeout of 0ms.
console.log(1);
setTimeout(() => console.log(2), 0);
console.log(3);
// logs 1, then 3, then (as soon as possible) logs 2
This is because setTimeout
always gets pushed to the back of the queue—the specified wait time just tells JS the minimum time that has to pass before that code is allowed to run.
We can use callbacks (functions passed as arguments to other functions) to access async values or run our code once some async task completes. In fact the first argument to setTimeout
above is a callback. We pass a function which setTimeout
runs once the timeout has finished.
Callbacks can be fiddly to deal with, and you may end up with very nested function calls if you have to chain lots of async stuff. Here's a contrived example:
getStuff((err, stuff) => {
if (err) handleError(err);
getOtherStuff((err, otherStuff) => {
if (err) handleError(err);
console.log(stuff, otherStuff);
});
});
Here's how that would look using promises:
getStuff().then(getOtherStuff).catch(handleError);
Promises are a special type of object. They allow us to represent the eventual result of async code. A function that executes async code will return the promise object instead of the final value (which it doesn't have yet).
For example when we fetch some data from a server we will receive a promise that will eventually represent the server's response (when the network request completes).
We can use the fetch
function to make HTTP requests in the browser. It takes two arguments: the URL you want to send the request to and an options object (we'll look at that later).
- Clone this repo and open
workshop.html
in your editor. Add your code inside the script tag. - Use
fetch
to make a request to"https://pokeapi.co/api/v2/pokemon/pikachu"
. - Assign the return value to a variable and log it.
- Open
index.html
in your browser. You should see the pending promise in the console.
Promises can be in 3 states:
- pending (async code has not finished yet)
- fulfilled (expected value is available)
- rejected (expected value is not available).
There's a bit more complexity to this, so it's worth reading this explanation of promise states later.
const myPromise = fetch("url");
console.log(myPromise);
// Promise { <state>: "pending" }
// or
// Promise { <state>: "fulfilled", <value>: theResult }
// or
// Promise { <state>: "rejected", <value>: Error }
// Note: different browsers may show promises differently in the console
So how do we actually access the value when the promise fulfills?
Since the promise's fulfilled value isn't accessible syncronously, we can't use it immediately like a normal JS variable. We need a way to tell JS to run our code once the promise has fulfilled.
const myPromise = fetch("url");
myPromise.then((someData) => console.log(someData));
Promises are objects with a .then()
method. This method takes a callback function as an argument. The promise will call this function with the fulfilled value when it's ready.
It's worth noting that you don't need to keep the promise itself around as a variable.
fetch("url").then((someData) => console.log(someData));
- Use
.then()
to access the result of your PokeAPI request. Log this to see what a JS response object looks like.
We can see the response object, but how do we get the body? The PokeAPI is returning some JSON, but fetch
can't assume this. We have to explicitly tell it to parse the JSON body using the response.json()
method. This is also async, which means it also returns a promise. We need to use another .then()
to access the JSON value.
fetch("url").then((response) =>
response.json().then((data) => console.log(data))
);
Nesting our .then()
s like this is getting us back into the same mess as with callbacks. Luckily promises have a nice solution to this problem.
The .then()
method always returns a promise, which will resolve to whatever value you return from your callback. This allows you to chain your .then()
s and avoid nested callback hell.
If your first .then()
returns a promise the next one won't run until the first fulfills.
fetch("url")
.then((response) => response.json())
.then((data) => console.log(data));
- Use
response.json()
to get the response body - Add another
.then()
to log the body. You should see a Pokémon object
Sometimes requests go wrong. We can handle errors by passing a function to the promise's .catch()
method. This will be run instead of the .then()
if the promise rejects.
fetch("broken-url")
.then((response) => console.log(response))
.catch((error) => console.log(error));
- Remove the URL from your fetch call. You should see the browser warn you about an "uncaught error"
- Add a
.catch()
to your code that logs the error instead
Note: you would usually want to do something useful with the error instead of just logging it.
We're going to use the fetch
function to get a user from the GitHub API. The API is free to access, but you might get rate-limited if you make too many requests. We can avoid this by generating an access token and including it in our request URL.
- Write a
getUser
function that takes a username argument - It should fetch that user's profile from
"https://api.github.com/users/{{USERNAME_HERE}}?access_token={{TOKEN_HERE}}"
- It should be callable like this:
getUser("oliverjam") .then((user) => console.log(user)) .catch((error) => console.log(error));
- Write a
getRepos
function that takes the Github user response object as an argument. - Fetch the a user using
getUser
, then usegetRepos
to fetch their repos using therepos_url
from the user object. - Log the array of repos.
- Fetch multiple GitHub profiles simultaneously using your
getUser
function above (you'll have to call it more than once)
You might want to read the docs for Promise.all