πŸ§‘πŸΏβ€πŸ’» prep

πŸ• Fetching data

Learning Objectives

So far we have displayed film data stored in our JavaScript code. But real applications fetch data from servers over the internet. We can restate our problem as follows:

Given an API that serves film data When the page first loads Then the page should fetch and display the list of film data, including the film title, times and film certificate

πŸ’» Client side and 🌐 Server side APIs

We will use fetch(), a client side Web API 🧢 🧢 client side Web API A client side Web API lives in the browser. They provide programmatic access to built-in browser functions from JavaScript. . Fetch will fetch our data from the server side API 🧢 🧢 server side API A server side API lives on a server. They provide programmatic access to data or functions stored on the server from JavaScript. .

APIs are useful because they let us get information which we don’t ourselves know. The information may change over time, and we don’t need to update our application. When we ask for the information, the API will tell us the latest version.

We also don’t need to know how the API works in order to use it. It may be written in a different programming language. It may talk to other APIs we don’t know about. All we need to know is how to talk to it. This is called the interface.

Using fetch is simple. But we want to understand what is happening more completely. So let’s take ourselves on a journey through time.

πŸ‘‰πŸΎ Unfurl to see the journey (we will explain this in little pieces)
graph TD fetch[(πŸ• fetch)] --> |sends a| Request{πŸ“€ Request} Request --> |has a latency| TimeProblem[πŸ—“οΈ Time Problem] Request --> |to| ServerAPIs fetch --> |is a| ClientAPIs TimeProblem --> |caused by| SingleThread[🧡 Single thread] Callbacks{{πŸͺƒ Callbacks}} --> |run on| SingleThread SingleThread --> |handled by| EventLoop[πŸ” Event Loop] EventLoop --> |queues| Callbacks SingleThread --> |send tasks to| ClientAPIs SingleThread --> |handled by| Asynchrony TimeProblem --> |solved by| Asynchrony[πŸ›ŽοΈ Asynchrony] Asynchrony --> |delivered with| Promise{{🀝 Promises}} Asynchrony --> | delivered with | ClientAPIs Promise --> |resolve to a| Response{πŸ“€ Response} Promise --> |join the| EventLoop{{Event Loop πŸ”}} Promise --> |syntax| async{{πŸƒβ€β™‚οΈ async}} async --> |syntax| await{{πŸ“­ await}} await --> |resolves to| Response Response ---> |sequence with| then{{βœ”οΈ then}} APIs((🧰 APIs)) --> |live in your browser| ClientAPIs{πŸ’» Client side APIs} ClientAPIs --> |like| setTimeout[(⏲️ setTimeout)] ClientAPIs --> |like| eventListener[(🦻🏾 eventListener)] APIs --> |live on the internet| ServerAPIs{🌐 Server side APIs} ServerAPIs --> |serve our| Data[(πŸ’Ύ Data)] Data --> |as a| Response

πŸ˜΅β€πŸ’« This is a lot to take in. Let’s break it down and make sense of it.

πŸ—“οΈ Latency

Learning Objectives

graph LR fetch[(πŸ• fetch)] --> |sends a| Request{πŸ“€ Request} Request --> |has a latency| TimeProblem[πŸ—“οΈ Time Problem]

Instead of already having our data, we are now sending a request over the network to another computer, and then waiting for that computer to send us a response back. Now that our data is going on a journey over a network, we introduce the problem of latency.

Latency is the time taken for a request to traverse the network.

πŸ’‘ Network latency is travel time.

Why is latency a problem? Because it means we need to wait for our data. But our program can only do one thing at a time - if we stopped our program to wait for data, then we wouldn’t be able to do anything else. We need to handle this time problem.

Programming often involves time problems, and latency is just one of them.

⏳ Asynchrony : outside time

Learning Objectives

We can handle latency using asynchronous execution 🧢 🧢 asynchronous execution run code in a different order. To understand asynchrony we first need to be clear about synchronous execution 🧢 🧢 synchronous execution run code in the order it is written. .

We have written a lot of JavaScript programs that execute sequentially. This means that each line of code is run in order, one after the other.

For example:

console.log("first");
console.log("second");
console.log("third");

Outputs:

first
second
third
Each line of code is run in order. This is synchronous execution. We do this because JavaScript is single threaded 🧢 🧢 single threaded A single thread can do one thing at a time. JavaScript is a single threaded language. .

When we call a function, the function will run to completion before the next line of code is executed. But what if we need to wait for something to happen? What if we need to wait for our data to arrive before we can show it? In this case, we can use asynchronous execution.

Event Loop

We have already used asynchronous execution. We have defined eventListeners that listen for events to happen, then execute a callback function. But here’s a new idea: eventListeners are part of the Event API. They are not part of JavaScript! 🀯 This means you can’t use them in a Node REPL, but they are implemented in web browsers. The core of JavaScript is the same everywhere, but different contexts may add extra APIs.

When you set an eventListener you are really sending a call to a Web API and asking it do something for you.

const search = document.getElementById("search");
search.addEventListener("input", handleInput);

The callback handleInput cannot run until the user types. With fetch, the callback function cannot run until the data arrives. In both cases, we are waiting for something to happen before we can run our code.

We use a function as a way of wrapping up the code that needs to be run later on. This means we can tell the browser what to do when we’re done waiting.

πŸ‘‰πŸ½ Visualise the Event Loop

🧠 Recap our concept map

graph LR TimeProblem[πŸ—“οΈ Time Problem] --> |caused by| SingleThread[🧡 Single thread] SingleThread --> |send tasks to| ClientAPIs TimeProblem --> |solved by| Asynchrony[πŸ›ŽοΈ Asynchrony] Asynchrony --> | delivered with | ClientAPIs{πŸ’» Client APIs} ClientAPIs --> |like| setTimeout[(⏲️ setTimeout)] ClientAPIs --> |like| eventListener[(🦻🏾 eventListener)] ClientAPIs --> |like| fetch[(πŸ• fetch)]

πŸͺƒ Callbacks

Learning Objectives

Consider this visualisation of an asynchronous program:

πŸ‘‰πŸ½ Code running out of order and off the thread

When we call setTimeout we send a function call to a client side Web API. The code isn’t executing in our single thread any more, so we can run the next line. The countdown is happening, but it’s not happening in our thread.

When the time runs out, our Web API sends a message to our program to let us know. This is called an event 🧢 🧢 event An event is a signal that something has happened. . Our API sends its message to our event loop 🧢 🧢 event loop The event loop is a JavaScript mechanism that handles asynchronous callbacks. . And what message does the event loop send? It sends a callback. It sends our call back. It tells our thread to run the code in that function.

πŸ’‘ tip

A callback is our function call, sent back to us through the event loop, for us to run.

With a pen and paper, draw a diagram of your mental model of the event loop.

Use your model to predict the order of logged numbers in the following code snippet:

setTimeout(function timeout() {
  console.log("1");
}, 2000);
setTimeout(function timeout() {
  console.log("2");
}, 500);
setTimeout(function timeout() {
  console.log("3");
}, 0);
graph Callbacks{{πŸͺƒ Callbacks}} --> |run on| SingleThread[🧡 Single thread] SingleThread --> |handled by| EventLoop[πŸ” Event Loop] EventLoop --> |queues| Callbacks SingleThread --> |send tasks to| ClientAPIs{πŸ’» Client APIs} ClientAPIs --> | send| Callbacks

Did yours look different? There are many ways to visualise the event loop. Work on building your own mental model that helps you predict how code will run.

🌐 Requesting from a server side API

Learning Objectives

So now we have these pieces of our giant concept map

  1. πŸ“€ we know that we can send a request using fetch()
  2. πŸ• we know that fetch is a πŸ’» client side 🧰 Web API
  3. πŸ—“οΈ we know that sending πŸ“€ requests over a network takes time
  4. 🧡 we know that we should not stop our program to wait for data
  5. πŸͺƒ we know that we can use callbacks to manage events

But we still don’t know how to use fetch to get data from a server side API. Let’s find this out now. In our filterFilms code, replace the films array with data fetched from a server.

// Begin with an empty state
const state = {
  films: [],
};
// Data
const endpoint = "//curriculum.codeyourfuture.io/dummy-apis/films.json";

const fetchFilms = async () => {
  const response = await fetch(endpoint);
  return await response.json();
}; // our async function returns a Promise

fetchFilms().then((films) => {
  render(filmContainer, films); // when
});

πŸ• fetch returns a πŸ«±πŸΏβ€πŸ«²πŸ½ ‍Promise; the πŸ«±πŸΏβ€πŸ«²πŸ½ Promise fulfils itself with a πŸ“₯ response; the response contains our πŸ’Ύ data.

We will dig into this syntax: Promises, async, await, and then in our next sprint and complete our concept map.

πŸ«±πŸΏβ€πŸ«²πŸ½ Promises

Learning Objectives

graph LR Asynchrony --> |delivered with| Promise{{🀝 Promises}} Promise --> |resolve to a| Response{πŸ“€ Response} Promise --> |join the| EventLoop{{Event Loop πŸ”}}

To get data from a server, we make a request with fetch. We act on what comes back: the response. But what happens in the middle? We already know that JavaScript is single-threaded: it can only do one thing at a time.

So do we just stop and wait? No! We have a special object to handle this time problem. Run this code in your Node REPL:

const url = "https://api.github.com/users/SallyMcGrath"; // try your own username
const response = fetch(url);
console.log(response);
Your Promise should look like this:
Promise {
  Response {
    [Symbol(realm)]: null,
    [Symbol(state)]: {
      aborted: false,
      rangeRequested: false,
      timingAllowPassed: true,
      requestIncludesCredentials: true,
      type: 'default',
      status: 200,
      timingInfo: [Object],
      cacheState: '',
      statusText: 'OK',
      headersList: [HeadersList],
      urlList: [Array],
      body: [Object]
    },
    [Symbol(headers)]: HeadersList {
      cookies: null,
      [Symbol(headers map)]: [Map],
      [Symbol(headers map sorted)]: null
    }
  },
  [Symbol(async_id_symbol)]: 54,
  [Symbol(trigger_async_id_symbol)]: 30
}

The response in this code is not labelling the data. It’s labelling a Promise.

A promise is exactly what it sounds like: a promise to do something. You can use this promise object to sequence your code. You can say, “When the data comes back, then do this.”

You will explore Promises in more detail as you build more complex applications. For now, let’s move on to .then().

πŸͺ† .then()

Learning Objectives

graph LR Promise{{🀝 Promises}} --> |resolve to a| Response{πŸ“€ Response} Response ---> |sequence with| then{{πŸͺ†οΈ then}}

.then() is a method that belongs to the Promise prototype 🧢 🧢 prototype A prototype object is like a template. then is a method available on any Promise. You can think of the commands as

  1. given a request to fetch some data
  2. when the response comes back / the promise resolves to a response object
  3. then do this next thing with the data / execute this callback

The .then() method takes in a callback function that will run once the promise resolves.

For example:

const url = "https://api.github.com/users/SallyMcGrath";
const callback = (response) => response.json(); // .json() is an instance method that exists for all Response objects.
fetch(url).then(callback);

We can also inline the callback variable here - this code does exactly the same as the code above:

const url = "https://api.github.com/users/SallyMcGrath";
fetch(url).then((response) => response.json());

It’s a similar idea as the event loop we have already investigated, but this time we can control it clearly. The .then() method queues up callback functions to execute in sequence once the asynchronous operation completes successfully. This allows us to write code as if it was happening in time order.

πŸ’‘ tip

The then() method of a Promise always returns a new Promise.

We can chain multiple .then() calls to run more logic, passing the resolved value to the next callback in the chain. This allows us to handle the asynchronous response in distinct steps. Let’s create a getProfile function which we can try out in our Node REPL:

const getProfile = (url) => {
  return fetch(url)
    .then((response) => response.json()) // This callback consumes the response and parses it as JSON into an object.
    .then((data) => data.html_url) // This callback takes the object and gets one property of it.
    .then((htmlUrl) => console.log(htmlUrl)); // This callback logs that property.
};
getProfile("https://api.github.com/users/SallyMcGrath");

So then returns a new Promise, and you can call then again on the new object. You can chain Promises in ever more complex dependent steps. This is called Promise chaining.

It’s important to understand some of what is happening with Promises and then. But for the most part, you will not be writing code in this style.

🍬 async/await

Learning Objectives

graph LR Promise{{🀝 Promises}} --> |syntax| async{{πŸƒβ€β™‚οΈ async}} async --> |syntax| await{{πŸ“­ await}} await --> |resolves to| Response{{πŸ“€ Response}}

Async/await is syntactic sugar 🧢 🧢 syntactic sugar A simpler, or “sweeter” way to write the same thing. The code works the same under the hood, but it’s easier to read. for Promises. We group them together: async/await, because we use them together. 🧢 🧢 use them together. We can only use await inside an async function or at the top level of a module.

We use the async keyword to define a function that returns a Promise. An async function always returns a Promise.

We can see this with a simple function which doesn’t need to await anything:

const getProfile = async (url) => url;

console.log(getProfile("hello")); // Logs a Promise.

getProfile("hello").then((value) => console.log(value)); // Logs a value

Even though the function above doesn’t have a time problem, the fact that we define the function as an async function means it returns a Promise.

But let’s do something more interesting - let’s actually solve a time problem.

const getProfile = async (url) => {
  // the async keyword tells us this function handles a time problem
};

We use the await operator to wait for a Promise to resolve. This allows us to write code that looks like it’s happening in time order, but doesn’t block our main thread.

const getProfile = async (url) => {
  const response = await fetch(url);
  return response.json();
};

Go ahead and call this in your Node REPL in your terminal: getProfile("https://api.github.com/users/SallyMcGrath").then(console.log). It works the same as before.

🫠 Handling errors

When we use await, we are saying, “Wait for this Promise to resolve before moving on to the next line of code.” But if the Promise doesn’t resolve, the next line of code will never run and an error will be thrown.

Let’s try this. Call getProfile with a url that doesn’t exist: getProfile("invalid_url");

You will get a curious response:

Uncaught (in promise) TypeError...
getProfile("invalid_url")
Promise {
  <pending>,
  [...]
}
> Uncaught [TypeError: Failed to parse URL from invalid_url] {
  [cause]: TypeError: Invalid URL
      [...] {
    code: 'ERR_INVALID_URL',
    input: 'invalid_url'
  }
}

Some lines redacted […] for clarity.

JavaScript is telling us we need to catch the error, but how, and why?

πŸ₯Ž try/catch

Learning Objectives

We can handle errors with a try/catch block. We can use the try keyword to try to do something, and if it fails, catch the error 🧢 🧢 error An Error is a global object produced when something goes wrong. We can throw an Error manually with the throw keyword. We can use try/catch in both synchronous and asynchronous code.

const getProfile = async (url) => {
  try {
    const response = await fetch(url);
    return response.json();
  } catch (error) {
    console.error(error);
  }
};

Let’s trigger an error to see this in action. In a Node REPL in your terminal, call getProfile on an API that does not exist again:

getProfile("invalid_url");

TypeError: Failed to parse URL from invalid_url
  [...]
  [cause]: TypeError: Invalid URL
  [...]
    code: 'ERR_INVALID_URL',
    input: 'invalid_url'

It’s actually the same error you saw before, without the word ‘Uncaught’ before it. But why do we care about this? It’s not obvious in this simple, single function. If we don’t catch the error, the function will crash. 🧢 🧢 crash. The JavaScript execution will halt with a fatal exception, causing the Node.js process to exit immediately. Any further statements will not be run.

You need to tell JavaScript what to do when something goes wrong, or it will give up completely. In fact, in synchronous programming, the entire program would crash. In asynchronous programming, only the function that threw the error will crash. The rest of the program will continue to run.

πŸ’‘ tip

Handle your errors in all cases.

πŸ• 🎞️ fetch films

Learning Objectives

Now that we have a basic understanding of Web APIs and Promises, let’s use our knowledge to get some data from an API. There’s a list of films stored in a JSON file in this directory. We’ll use fetch to get the data from this API and then render it to the page.

🎯 Success criterion: You have a working app that fetches data from an API and renders it to the page.

🧠 Think back to your filterFilms project.

  1. Find your completed code. You’re going to iterate on this code to fetch the films from the API instead of using the data in the file.
  2. Update the state to start with an empty array. We can’t work with films we haven’t fetched yet!
const state = {
  films: [],
};
  1. Make a new getFilms function to use fetch to get the data from the API. The URL is //curriculum.codeyourfuture.io/js3/blocks/fetch-films/data.json

  2. Use:

  • fetch to get the data
  • async/await to make sure the function waits for the fetch to complete before trying to get the json data from the response
  • response.json() to get the data from the response
  • a try...catch block to handle any errors that might occur
const getFilms = async () => {
  try {
    const response = await fetch(
      "//curriculum.codeyourfuture.io/js3/blocks/fetch-films/data.json"
    );
    return await response.json();
  } catch (error) {
    console.error(error);
    return [];
  }
};

We’ve added a try...catch block to handle any errors that might occur. We’ve also added await to the fetch and response.json() calls. This means that the function will sensibly wait for the fetch to complete before trying to get the json data from the response.

In our last implementation, we called the render function straight away. This time, we need to wait for the films to be fetched before we can render them. Write a new async function to initialise our app. Try to write it yourself first, then check your understanding below.

Your init function should look something like this:

// Initial render, which is distinct from the render function as it loads our films into memory from the API.
// Subsequent render calls do not need to call the API to get the films - we already know the films and can remember them.
async function init() {
  try {
    const films = await getFilms();
    state.films = films;
    render(filmContainer, films);
  } catch (error) {
    console.error(error);
  }
}

The name init is a convention. It has no special meaning in the JavaScript language.

🎁 Finally!

And let’s now call this function at the end of our script.

init();

πŸ’‘ tip

🧧 Here’s an example implementation you can download.