Asynchronous JavaScript

JavaScript , Thu Jan 12 2023

    Intervals and Timers

    Welcome to a new module in this post! I am super excited for this one, because now we're going to understand some important concepts in Javascript. These concepts will later help us provide functionalities to our app like fetching data and sending data to servers.

    Until now, in this post we have been dealing with something called synchronous javascript and it's now time to understand asynchronous JS.

    You might not understand these terms, but by the end of this lecture you will know what are they and why these concepts are crucial to building real world apps. So, let's get started!

    In JavaScript, there is a variety of pre-made functions that allow you to execute chunks of code in timed intervals, even while other code in the program is being executed.

    Imagine you're coding a video game and need a function that could execute certain drawing operations every millisecond, or a local clock that ticks the amount of time a user has spent on your site; you'd probably help yourself with the following:

    setInterval():

    The setInterval allows you to execute a chunk of code every time a specified amount of milliseconds passes.

    For example; this code logs "Hello World" every thousand milliseconds:

      setInterval(()=>{
        console.log('Hello World');
      },1000);

    That's great and all, but how do you prevent an interval from going on forever? Or store one for that matter. Well, any interval can be stored as a full variable that can later be cleared using the clearInterval() function. A more professional take on the example above would then be:

      const myInterval = setInterval(() => console.log('Hello World'),1000);
      clearInterval(myInterval);

    The clear function is especially useful if you only want an interval to execute a certain amount of time, and clear it once a condition (such as a milisecond counter getting to a certain value) is reached.

    setTimeout():

    The setTimeout function allows you to wait a certain amount of time before executing a chunk of code, do note that other code outside of the timeout will continue execution as normal. The way it's used is identical to setInterval.

      const myTimeout = setTimeout(() => console.log('Hello World'),1000); // console log's "Hello World" after a thousand ms
      clearTimeout(myTimeout);

    It is cleared using the function clearTimeout();

    This is the first time you see JavaScript code that doesn't execute linearly, from top to bottom. It is asynchronous. The code on top can be executed after the code on the absolute bottom of the file. Later in the course, we're going to go into much more depth in terms of asynchronous JavaScript.

    Introduction to Asynchronous JavaScript

    Synchronous JS Example

    What is synchronous Javascript?

    Synchronous Javascript is one in which the code is executed line by line and their tasks are completed instantly, i.e. there is no time delay in the completion of the tasks for those lines of code.

    First let me give you an example of synchronous javascript:

      const functionOne = () => {
        console.log('Function One');
        functionTwo();
        console.log('Function One, Part 2');
      };
      const functionTwo = () => {
        console.log('Function Two');
      };
      functionOne();
      // Function One
      // Function Two
      // Function One, Part 2

    As expected, first 'Function one.' is logged and then fnTwo is invoked, so 'Function two.' is logged and then back in fnOne, 'Function one, part 2' is logged. Pretty straight and simple, isn't it?

    Let's change the functions

    It's time to give you a taste of asynchronous javascript! So, let's change the functions we wrote:

      const functionOne = () => {
        console.log('Function One');
        functionTwo();
        console.log('Function One, Part 2');
      };
      const functionTwo = () => {
        setTimeout(() => console.log('Function Two'),2000);
      };
      functionOne();
      // Function One
      // Function One, Part 2
      // (after two second delay)
      // Function Two

    Here, in fnTwo instead of a normal console.log, we will use a setTimeout in order to be able to fake a time delay that happens when we are fetching data from servers or interacting with APIs (Application Programming Interfaces).

    So, for the setTimeout we will have a callback function wherein we will log 'Function two.' to the console. We will keep 2000 millisecond delay.

    So, most probably you would be surprised that the code didn't behave the way you thought it should and it's completely normal to be wrong here, right now.

    Here, you might think, why does the javascript engine not wait for the setTimeout to end and then continue. So, let's say, we might have some asynchronous code in our script and after those lines we have some code to handle DOM events.

    So if the JS engine stops for the things which take time, the users might interact with the webpage at that time and those events will remain unhandled leading the user to think that the website in not working! That is not good. So you see that, this feature is in fact in our favor, we just have to handle these kind of functions differently.

    So , let's define Asynchronus Javascript

    Asynchronous Javascript is one in which some lines of code take time to run. These tasks are run in the background while the Javascript engine keeps executing other lines of code. When the result of the asynchronous tasks gets available, it is then used in the program.

    So, the main concept behind Asynchronous Javascript is that we don't wait for a function to get executed and complete its task and then handle the result.

    But, we simply let the async function do its job in the background and we move on to execute the other lines of code and then use the result of that asynchronous task when it is available.

    Async JavaScript and Callback Hell

    In this section we're going to cover a lot of advanced JavaScript concepts, some of which are: API data fetching, asynchronous code, callback functions, promises and async/await.

    This section is going to teach you how you can first simulate or create that asynchronous source and then how we can properly deal with the data coming out of it.

    Let's immediately dive in into a real example of asynchronous JavaScript, and that is: data fetching. With JavaScript, we can fetch the data from a range of different API's. API stands for Application Programming Interface & it is simpliy something that you can access data from

    Once we fetch the data, depending on the size of the data we're fetching and our internet speed, the fetching is going to take a certain amount of time.

    Opposed to the setTimeout, where we always waited for 2 seconds, with real data fetching, we cannot be sure how long is it going to take.

    Now we're going to simulate that data fetching.

    Let's say that we're trying to fetch a user from the database.

    This is the problem:

      const fetchUser = (username) => {
        setTimeout(() => {
          return {user: username};
        },2000); // we don't know the time, but let's say 2000
      }
      const user = fetchUser('test');
      console.log(user); // undefined

    The reason because we got undefined is that the data wasn't return from the function immediately. It waited 2 seconds. How can we fix this?

    Here comes the concept of Callback Functions.

    We can pass in a callback function that's going to run when the data is fetched

      const fetchUser = (username, callback) => {
        setTimeout(() => {
          console.log('Now we have the user');
          callback({user: username});
        },2000); // we don't know the time, but let's say 2000
      }
      const user = fetchUser('test', (user) => {
        console.log(user);
      });

    Our fetch user function accepts a callback function as a parameter, and that's where we get the data.

    Async JavaScript & Callbacks Part 2

    This was just a simple example, but now, let's add more things onto it, because later on, we're going to exchange callback functions for both Promises and Async/Await.

    To complicate it, we're going to imagine that we're working on a social media platform of sorts. Once the user profile is fetched, then we want to fetch his photos.

    So let's create a function for that:

      const fetchUser = (username, callback) => {
        setTimeout(() => {
          console.log('Now we have the user');
          callback({user: username});
        },2000); // we don't know the time, but let's say 2000
      }
      const fetchUserPhotos = (username, callback) => {
        setTimeout(() => {
          console.log('Now we have the photos');
          callback(['photo1',['photo2']]);
        },2000); // we don't know the time, but let's say 2000
      }
      const user = fetchUser('test',(username) =>{
        console.log(username);
        fetchUserPhotos(username, (userPhotos)=>{
          console.log(userPhotos);
        });
      });

    This is already getting messy.

    Let's add just one more function, and you'll easily notice the problem.

      const fetchUser = (username, callback) => {
        setTimeout(() => {
          console.log('Now we have the user');
          callback({user: username});
        },2000); // we don't know the time, but let's say 2000
      }
      const fetchUserPhotos = (username, callback) => {
        setTimeout(() => {
          console.log('Now we have the photos');
          callback(['photo1',['photo2']]);
        },2000); // we don't know the time, but let's say 2000
      }
      const fetchPhotoDetails = (photo, callback) => {
        setTimeout(() => {
          console.log('Now we have the photo details');
          callback('details...');
        },2000); // we don't know the time, but let's say 2000
      }
      const user = fetchUser('test',(username) =>{
        console.log(username);
        fetchUserPhotos(username, (userPhotos)=>{
          console.log(userPhotos);
          fetchPhotoDetails(userPhotos[0],(details)=>{
            console.log(details);
          });
        });
      });

    As you can see, if we use callbacks, we get this weird structure that just keeps moving to the right. If we added a few more callbacks, this is how it would look:

      const user = fetchUser('test',(username) => {
        fetchUserPhotos(username,(userPhotos) => {
          fetchPhotoDetails(userPhotos[0],(details) => {
            fetchPhotoDetails(userPhotos[0],(details) => {
              fetchPhotoDetails(userPhotos[0],(details) => {
                fetchPhotoDetails(userPhotos[0],(details) => {
                  console.log(details);
                });
              });
            });
          });
        });
      });

    This is called callback hell. It becomes unreadable.

    In this code, we can see on the left side, there's a triangle like structure to the indentation, this is infamously known as THE CALLBACK HELL. So what callbacks includes is:

    In short, every function gets an argument which is another function that is called with a parameter that is response from the previous one.

    You will definitely get baffled with this sentence, which describes the CALLBACK HELL.

    As in our case, you can only imagine, to display several results how many callback functions we will have to make. It's difficult to manage a lot of callback functions.

    Even if you wrote them yourself, you're going to have a hard time understanding them once you come back to the code after some time!

    This pattern of coding (i.e. callbacks) at a large scale is not maintainable and is confusing and also violates the DRY principle and hence is a bad practice to follow.

    So to resolve this issue, Javascript introduced the concept of promises. In the next section, you're going to fully understand how promises work, and we're going refactor this code to use promises. Stay tuned!

    Promises

    In the last section, we've witnessed the callback hell. Now promises come in to the rescue.

    What are promises? They are objects that either return the successfully fetched data, or the error.

    Let's try to create one:

      // Creating of promise   const promise = new Promise((resolve, reject) => {
        setTimeout(()=>{
          console.log('Got the user');
          resolve({user: 'Adrian'});
        },2000);
      });
      // getting the data from the promise
      promise.then((user) => {
        console.log(user);
      });

    As you can see, this is much much easier to read.

    Keep in mind that during the callbacks example, we didn't even test for the errors, when making requests, sometimes, the data may not come back. The reason can be your internet connection, or maybe you're just fetching the data from the wrong database.

    In any case, the fact is, that sometimes, you're not going to receive the data you were looking for. And we need to handle those cases. With promises, it's easy.

    We'll just replace the resolve with the reject, and pass our error message.

      // Creating of promise
      const promise = new Promise((resolve, reject) => {
        setTimeout(()=>{
          console.log('Got the user');
          // resolve({user: 'Adrian'});
          reject('Error');
        },2000);
      });
      // getting the data from the promise
      // .then gives us the result of the resolve
      // and .catch gives us the result of the reject
      promise
      .then((user) => {
        console.log(user);
      })
      .catch((error) => {
        console.log(error);
      });

    In the last example, with callback functions, we had a lot of functions.

    How would we do that with promises?

      console.log(1);
      const fetchUser = (username) => {
        return new Promise((resolve, reject)=>{
          setTimeout(()=>{
            console.log('Now we have the user');
            resolve(username);
          },2000);
        });
      };
      const fetchUserPhotos = (username) => {
        return new Promise((resolve, reject)=>{
          setTimeout(()=>{
            console.log('Now we have the photos');
            resolve(['photo1','photo2']);
          },2000);
        });
      };
      const fetchPhotoDetails = (photo) => {
        return new Promise((resolve, reject)=>{
          setTimeout(()=>{
            console.log('Now we have the photo details');
            resolve('details...');
          },2000);
        });
      };
      // not this
      const user = fetchUser('test',(username)=>{
        console.log(username);
        fetchUserPhotos(username,(userPhotos)=>{
          console.log(userPhotos);
          fetchPhotoDetails(userPhotos[0],(details)=>{
            console.log(details);
          });
        });
      });
      // and finally, we would call it like this:
      fetchUser('Adrian')
        .then((user)=>fetchUserPhotos(user))
        .then((photos)=>fetchPhotoDetails(photos))
        .then((detail)=>console.log('detail'));
      console.log(2);

    This is the same thing that we have above. But notice how much easier this is to both read and write. We never again have to use callbacks.

    Promises are enabling us to write asynchronous code in a much easier way. Recently, there has been an addition to promises. It's called async away, we're going to check it out next.

    Async/Await

    What is Async-Await?

    Async await is simply an addition to promises, it is an easier and cleaner way to work with promises.

    The main advantage of asynchronous functions is that they look and behave more like synchronous functions we're all used to. Because of that, it's easier to work with them.

    Lets take a look at a simple example

      const fetchNumber = async()=>{
        return 25;
      }
      asyncOne().then(result => {
        console.log(result); // should log 25
      });

    Running the above code logs 25 to the console. This means that the promise was fulfilled then returned. If it had not the .then() method could not have worked.

    Now we come to the special await keyword. The await waits for the promise to return a result.

    The await keyword can only be used inside of an async function.

    Furthermore usage of the await expression does not stop the program from running. Rather it only make the functions block with the async keyword wait until a promise is fulfilled.

    To go back to our initial example with callbacks and promises, let's do it with async/await. Async await still uses promises, but with a nicer syntax.

      const displayData = async() =>{
        const user = await fetchUser('Adrian');
        const photos = await fetchUserPhotos(user);
        const detail = await fetchPhotoDetails(photos[0]);
        console.log(detail);
      };

    Look how clean and simple it looks. Amazing!