/learn-fetch

Learn how to make HTTP requests in the browser (using fetch and promises)

Primary LanguageHTML

Learn Fetch & Promises

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);
});

Asynchronicity

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.

Callbacks

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);

What is a promise?

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).

fetch

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).

Challenge 1

  1. Clone this repo and open workshop.html in your editor. Add your code inside the script tag.
  2. Use fetch to make a request to "https://pokeapi.co/api/v2/pokemon/pikachu".
  3. Assign the return value to a variable and log it.
  4. Open index.html in your browser. You should see the pending promise in the console.

Promise terminology

Promises can be in 3 states:

  1. pending (async code has not finished yet)
  2. fulfilled (expected value is available)
  3. 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?

Accessing the promise value

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));

Challenge 2

  1. Use .then() to access the result of your PokeAPI request. Log this to see what a JS response object looks like.

Accessing the response body

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.

Chaining .then

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));

Challenge 3

  1. Use response.json() to get the response body
  2. Add another .then() to log the body. You should see a Pokémon object

Handling errors

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));

Challenge 4

  1. Remove the URL from your fetch call. You should see the browser warn you about an "uncaught error"
  2. 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.


Workshop

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.

Task 1

  1. Write a getUser function that takes a username argument
  2. It should fetch that user's profile from "https://api.github.com/users/{{USERNAME_HERE}}?access_token={{TOKEN_HERE}}"
  3. It should be callable like this:
    getUser("oliverjam")
      .then(user => console.log(user))
      .catch(error => console.log(error));

Task 2

  1. Write a getRepos function that takes the Github user response object as an argument.
  2. Fetch the a user using getUser, then use getRepos to fetch their repos using the repos_url from the user object.
  3. Log the array of repos.

Bonus if you have time: Task 4

  1. 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