Skip to content

Instantly share code, notes, and snippets.

@adhikarisushant
Last active April 21, 2024 10:32
Show Gist options
  • Save adhikarisushant/edc0935ae43999ab7a55e7cd6a8cfa2b to your computer and use it in GitHub Desktop.
Save adhikarisushant/edc0935ae43999ab7a55e7cd6a8cfa2b to your computer and use it in GitHub Desktop.
Get along with advanced javascript concept.

Asynchronous JavaScript

1. Thread Of Execution

JavaScript is Synchronous

We are going to start to get into asynchronous JavaScript. Before we do that though, it is important to understand how JavaScript actually works and to understand that at it's core, JavaScript is a synchronous and single-threaded language. It has asynchronous capabilities, which we will be looking at in this section and others, but it is not asynchronous by default.

JavaScript is Single-Threaded

You will have a leg up on this stuff, if you know about the execution context. The execution context contains the currently running code and everything that aids in its execution. It also runs on what we call a thread of execution. The thread of execution is a single thread that runs one line of code at a time. This is why JavaScript is a single-threaded language and is synchronous. Some languages have multiple threads that can run code at the same time. JavaScript does not.

If we look at this image, it shows the thread of execution, which is a single sequential flow of control and each operation happens one after the other. The second console.log() will not run until the first one is finished. The third will not run until the second is finished. This is synchronous behavior. The thread also includes the call stack and memory heap.

2. How Asynchronous JavaScript Works: The Task Queue & Event Loop

So you know that JavaScript is a synchronous and it's a single-threaded language. It runs one line of code at a time. Just to remind you, we can look at the image we talked about in the last section.

These operations run line by line and each one has to wait for the last one to complete before executing. Where we run into issues is where one of these operations takes a while. Something like fetching data from a server or if you're using Node.js, maybe you're reading from or writing to a file. That can take a while, and by a while, I mean usually a few seconds or even a few milliseconds, but that's a long time in programming.

Blocking & Non-Blocking Code

When an operation takes a while and basically holds up the line, that is called blocking code or operations. It blocks the flow of the program until it completes. Non-blockingcode refers to code that does not block execution.

Let's look at the following code. Don't worry if you don't understand everything, but both pieces of code are reading a file using Node.js file system methods and then calculating the sum of numbers from 1 to 10.

On the left, we are using the readFileSync() method. This is a blocking method. It will read the file and then move on to calculate the sum. In the console, we see the file contents and then the sum.

On the right, we are using the readFile() method. This is a non-blocking, asynchronous method. It will read the file but it will not block the execution of the program by making it wait. The way it works, is it takes in a callback function and when the file is read, it will execute the callback function. In the console, we see the sum first and then the file contents, because we did not have to wait for the file to be read before calculating the sum.

Now readFile() is not available to us in the browser, but there are a lot of APIs that are available that work in a similar asynchronous way.

I'm going to show you a diagram to try and explain what happens when we write asynchronous code using these web APIs.

The yellow box represents the JavaScript engine. This is the part of the browser that executes our JavaScript code. This is where our call stack is that executes all of our functions, etc. This is also where the memory heap is, which is where all of our variables and objects are stored.

Outside of that, in the green box, we have a bunch of web APIs that are accessible to us via the browser and the global object. Remember, this is created during the creation phase of the global execution context.

If we go to the browser console and type in window and hit enter, you will see setTimeout() and setInterval() and a bunch of other functions that allow us to do things asynchronously.

In addition to setTimeout() and setInterval(), we have the whole DOM API. We select elements and put event listeners on them. That's another API we have available to us. it's not part of JavaScript. When you use Node.js, you don't have access to the document object, because there is no DOM or browser in Node.js. But as you saw in the code examples, in Node, you do have access to the filesystem API, which allows you to read and write files.

fetch() is another API that we have access to. It allows us to make HTTP requests. We'll be working with the fetch API quite a bit to send requests to APIs and services. This is something that a front-end developer has to know how to use, and we'll get to that soon enough.

Right now, I just want you to understand that this is stuff provided to us by the browser environment. Now, let's talk about how they work with the JavaScript engine, which is inherently synchronous.

Task Queue

So we know these APIs are separate from the JavaScript engine. We know that we have the ability to go off and do something while the script continues to execute and when that something is done, we fire off a callback function. This is possible because of something called the task queue.

When we call an asynchronous function such as setTimeout(). In the diagram it's being called from a function, but it could just as well be from the global scope. When we call it, we added a callback function as an argument. It then registers that callback and it gets added to what we call a task queue. This is a queue of callbacks that are waiting to be added to the call stack and executed.

A queue is a data structure that follows the first in, first out principle. This means that the first item that is added to the queue will be the first item that is removed from the queue. In our case, the first callback that is added to the task queue will be the first callback that is executed.

Remember, we already looked at a stack, which follows the last in, first out principle. This means that the last item that is added to the stack will be the first item that is removed. The call stack is an example. So both queues and stacks are data structures that are used in programming.

Event Loop

Any callbacks that are in the queue, have to be put on the stack to be executed. This is where the event loop comes in. The event loop is a process that checks the call stack and the task queue. If the call stack is empty, it will take the first callback in the task queue and add it to the call stack to be executed. When we create event listeners with addEventListener(), we are also adding callbacks to the task queue.

You can think of the event loop like one of those revolving doors at the mall. It's constantly checking to see if the door is open and if it is, it will let people in. If it's not, it will keep spinning until it is. In this case, we're dealing with functions instead of people.

Now this is also how the event loop and the task queue work within Node.js. There are different APIs and functions available, but it all works the same under the hood. Node even uses the same V8 engine that Chrome uses.

Promises

Now Just to confuse you a little bit more, with things like event listeners and setTimeout(), callbacks get added to the task queue. When we work with let's say, the fetch API, we get a Promise object back, which work a little differently.

Promises are objects that represent the eventual completion or failure of an asynchronous operation. They are a way to handle asynchronous code in a more elegant way than using callbacks. We'll be working with promises a lot in our career. The reason I'm mentioning them now is because promises create what are called PromiseJobs or the v8 engine calls them microtasks.

Microtasks are callbacks that are added to the microtask queue. It works in a similar way to the task queue, but it's a separate queue and it's checked before the task queue. It has a higher priority. There are also something called observers that are added to the microtask queue. We'll get to those later.

Alright, I know this is confusing as hell, but the truth is, you don't need to understand all of this right now. In fact, I know senior developers that don't know some of this stuff, but I wanted you to get a head start on what is actually happening under the hood.

3. setTimeout() and clearTimeout()

In the last section, we looked at a diagram that represented what actually happens under the hood when we use some of the asynchronous APIs that the browser or Node.js offers. In this section, we're going to look at setTimeout(), which is really helpful for doing something after a certain amount of time. We're also going to look at clearTimeout(), which is a function that we can use to cancel a timeout.

setTimeout(function () {
  console.log('Hello from callback');
}, 2000);

console.log('Hello from the top-level code');

When we run this code, we see the following output.

Hello from the top-level code
Hello from callback

The reason for this is because setTimeout() is given a number of milliseconds as the second argument and then it waits that amount of time and fires off. It does not block the code though, so we see the top-level console log first and then the callback console log.

Let's go ahead and change the 2000 milliseconds to 0. What do you think is going to happen?

setTimeout(function () {
  console.log('Hello from callback');
}, 0);

console.log('Hello from the top-level code');

When we run this code, we see the following output.

Hello from the top-level code
Hello from callback

You may have thought that the callback would execute first since we set the timeout to 0. Remember, that callback gets put on to the task queue and then it waits for the call stack to be empty. So, the callback is not going to execute until the call stack is empty, even if we set the timeout to 0. Just to remind you, here is the diagram that we looked at in the last section.

You may want to use this to change something in the DOM after a certain amount of time. We did this in the loan calculator project to show a spinner for 1 second before showing the results.

Let's make the h1 tag change after a few seconds.

setTimeout(() => {
  document.querySelector('h1').textContent = 'Hello from callback';
}, 3000);

We could also put that in a separate function and then call it.

function changeText() {
  document.querySelector('h1').textContent = 'Hello from callback';
}

setTimeout(changeText, 3000);

clearTimeout()

In addition to setTimeout(), we also have clearTimeout(). This is a function that we can use to cancel a timeout.

Let's create a button that will cancel the timeout to change the text.

<button id="cancel">Cancel Text Change</button>

In order to know which timeout to cancel, we need to store the id that is returned from setTimeout().

const timerId = setTimeout(changeText, 3000);

Now, we can create an event listener for the button that will call clearTimeout().

document.querySelector('#cancel').addEventListener('click', () => {
  clearTimeout(timerId);
  console.log('Timer Cancelled');
});

4. setInterval() and clearInterval()

setInterval() is used to run a specific callback function and repeat it at a set interval. The number of miliseconds passed to the function is the amoount of time to wait between each function call. Let's look at a simple example

const intervalID = setInterval(myCallback, 1000);

function myCallback() {
  console.log(a, Date.now());
}

This will log the timestamp every second.

We can also pass in parameters

const intervalID = setInterval(myCallback, 1000, 'Hello');

function myCallback(a) {
  console.log(a, Date.now());
}

clearInterval()

To clear or stop the interval, we can use clearInterval() and pass in the interval ID

clearInterval(intervalID);

Let's create a script to change the body background color every second. We will have buttons to start and stop it.

let intervalID;

function startChange() {
  if (!intervalID) {
    intervalID = setInterval(changeBackground, 1000);
  }
}

function changeColor() {
  if (document.body.style.backgroundColor !== 'black') {
    document.body.style.backgroundColor = 'black';
    document.body.style.color = 'white';
  } else {
    document.body.style.backgroundColor = 'white';
    document.body.style.color = 'black';
  }
}

function stopChange() {
  clearInterval(intervalID);
}

document
  .getElementById('start')
  .addEventListener('click', startChange);
document.getElementById('stop').addEventListener('click', stopChange);

We could make it a random color by generating a hex value

function changeRandomColor() {
  const randomColor = Math.floor(Math.random() * 16777215).toString(16);
  document.body.style.backgroundColor = `#${randomColor}`;
}

5. Callbacks

Let's touch on callback functions a bit more. A callback is simply a function that is passed into another function as an argument and executed within the function that it was passed into.

We have already used callbacks quite a few times in this tutorial. For example, we've used them with addEventListener() and setTimeout().

Just because a function takes in a callback does not mean that it is asynchronous. It is a way to handle asynchronous code, such as we saw with setTimeout(), where the callback is placed in the task queue and then it waits for the call stack to be empty before it is executed. But, we also used callbacks with high order array methods like forEach() and map(). These are not asynchronous. The callbacks are executed immediately in this case.

Callback Recap

addEventListener() is a good example of a function that takes in a callback. Let's look at the following code.

function toggle(e) {
  const bgColor = e.target.style.backgroundColor;

  if (bgColor === 'red') {
    e.target.style.backgroundColor = '#333';
  } else {
    e.target.style.backgroundColor = 'red';
  }
}

document.querySelector('button').addEventListener('click', toggle);

Notice, when we pass in a callback, we do not use parentheses. We just pass in the name of the function. Parentheses are used when we want to execute the function. The function is executed within the addEventListener() function at a later time (when the event occurs).

If we were to add parentheses, the function would execute immediately and the callback would not be passed into the addEventListener() function.

function toggle(e) {
  // Add this
  console.log('toggle ran...');

  const bgColor = e.target.style.backgroundColor;

  if (bgColor === 'red') {
    e.target.style.backgroundColor = '#333';
  } else {
    e.target.style.backgroundColor = 'red';
  }
}

document.querySelector('button').addEventListener('click', toggle());

If you run the code above, you will see the console log right away. You will also get an error, because it can't read the event object.

Implementing Callbacks

Until you are writing advanced JavaScript, you probably will not have too many times where you will actually create a function that takes in a callback, but let's try it, just to see how it works.

Let's create a couple posts inside of an array:

const posts = [
  { title: 'Post One', body: 'This is post one' },
  { title: 'Post Two', body: 'This is post two' },
];

Now I am going to create two functions. One to create a new post and one to get all posts. The createPost() function is going to create after two seconds and the getPosts() function is going to get all posts after one second.

function createPost(post) {
  setTimeout(() => {
    posts.push(post);
  }, 2000);
}

function getPosts() {
  setTimeout(() => {
    posts.forEach(function (post) {
      const div = document.createElement('div');
      div.innerHTML = `<strong>${post.title}</strong> - ${post.body}`;
      document.querySelector('#posts').appendChild(div);
    });
  }, 1000);
}

createPost({ title: 'Post Three', body: 'This is post three' });

getPosts();

We ran both functions, yet we only see the initial two posts. The third never shows up because the posts already showed up after one second then the createPost() function ran after two seconds. We need to use a callback to fix this.

We can use a callback to fix this. We can pass in a callback to the createPost() function and then call the getPosts() function inside of the callback.

function createPost(post, cb) {
  setTimeout(() => {
    posts.push(post);
    cb();
  }, 2000);
}

function getPosts() {
  setTimeout(() => {
    posts.forEach(function (post) {
      const div = document.createElement('div');
      div.innerHTML = `<strong>${post.title}</strong> - ${post.body}`;
      document.querySelector('#posts').appendChild(div);
    });
  }, 1000);
}

createPost({ title: 'Post Three', body: 'This is post three' }, getPosts);

Now, when we run the code, we see all three posts. The createPost() function is executed and then the callback is executed.

Where we can get in trouble with callbacks is when we have multiple callbacks nested within eachother. This is called callback hell. To address this, we can instead use something called promises, which we will get into a little later.

In the next ;lesson, I want to get into HTTP requests. Making HTTP requests will give us more realistic examples of asynchronous code, rather than just using setTimeout().

6. Callback Hell

So, we have seen a bunch of examples of callback functions. They come in handy when we want to make sure that some code is executed after another piece of code has finished executing. But what if we want to make sure that some code is executed after multiple pieces of code have finished executing? This can sometimes result in a lot of nested callbacks, which is called callback hell. There are something called promises that can help us with this. We will look at promises in the next section, but let's create a situation where we have to have multiple callback functions nested within each other.

I want to create a function called getData() that we can use to pass in an endpoint, whether a URL or a file path, and then it will make a request to that endpoint and return the data. We will use the XMLHttpRequest object to make the request. Right now, we will just log the data from the function. No callback is being passed in. Let's go ahead and do that.

function getData(endpoint) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', endpoint);

  xhr.onreadystatechange = function () {
    if (this.readyState === 4 && this.status === 200) {
      console.log(JSON.parse(this.responseText));
    }
  };

  setTimeout(() => {
    xhr.send();
  }, Math.floor(Math.random() * 3000) + 1000);
}

Now, you never know how long a request will take, so in addition to the request, I am adding a setTimeout() that will return the response within 1-3 seconds.

Now Let's create some .json files to fetch. These will just be local files, but it could just as well be a URL endpoint.

File 1 - movies.json

[
  {
    "title": "Scarface",
    "release_year": "1983"
  },
  {
    "title": "The Godfather",
    "release_year": "1972"
  },
  {
    "title": "Goodfellas",
    "release_year": "1990"
  },
  {
    "title": "A Bronx Tale",
    "release_year": "1993"
  }
]

File 2 - actors.json

[
  {
    "name": "Al Pacino",
    "age": "78"
  },
  {
    "name": "Robert De Niro",
    "age": "76"
  },
  {
    "name": "Joe Pesci",
    "age": "77"
  },
  {
    "name": "Chazz Palminteri",
    "age": "62"
  }
]

File 3 - directors.json

[
  {
    "name": "Brian De Palma",
    "age": "78"
  },
  {
    "name": "Francis Ford Coppola",
    "age": "82"
  },
  {
    "name": "Martin Scorsese",
    "age": "76"
  },
  {
    "name": "Robert De Niro",
    "age": "76"
  }
]

Now, let's say that we want to get the data from all 3 files. Let's do that.

getData('movies.json');
getData('actors.json');
getData('directors.json');

So, the way that we are doing it now, you'll notice that the order that we get the data is not the same order that we are requesting the data. This is because the setTimeout() is randomizing the order that the data is returned.

If we want to make sure that the data is returned in the order that we requested it. We can do that by passing in a callback function. Let's change the getData() function to accept and run a callback function.

function getData(endpoint, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', endpoint);

  xhr.onreadystatechange = function () {
    if (this.readyState === 4 && this.status === 200) {
      cb(JSON.parse(this.responseText));
    }
  };

  setTimeout(() => {
    xhr.send();
  }, Math.floor(Math.random() * 3000) + 1000);
}

Now, we can pass in a callback function when we call the getData() function.

getData('./movies.json', (data) => {
  console.log(data);
  getData('./actors.json', (data) => {
    console.log(data);
    getData('./directors.json', (data) => {
      console.log(data);
    });
  });
});

So we can see the issue here. We have nested 3 callback functions within each other. This is called callback hell. It is not very readable and it can get very messy very quickly. However it does work, it gets the data in the correct order.

7. Promises

Alright, so now we are going to learn about promises. A promise is an object that represents the eventual completion or failure of an asynchronous operation. The concept is that a promise is made to complete some kind of task or operation, such as fetching data from a server. Meanwhile, the rest of the code continues to execute. So it's asynchronous and non-blocking. When the task is complete, the promise is either fulfilled or rejected. It also prevents callback hell, which are multiple nested callbacks, as we saw in the previous section.

Most of the time, until you get into more advanced JavaScript, you will be dealing with the response from promises, not writing them. For instance, using the fetch API will return a promise. So you will need to know what to do with it. In this section, I will show you how to deal with them but also how to create them with the Promise constructor.

I'm going to show you how to refactor our posts code from callbacks to promises in the next section, but first I want to give you a super simple example of creating and dealing with promises, so that you can understand the syntax and the concept in general.

Creating a Promise

We use the Promise constructor to create a new promise. The Promise constructor takes in a function that has two parameters, resolve and reject. The resolve function is called when the promise is successful and the reject function is called when the promise is not successful.

Let's create a simple promise:

const promise = new Promise(function (resolve, reject) {
  // Do an async task
  setTimeout(function () {
    console.log('Async task complete');
    resolve();
  }, 1000);
});

So we used the Promise constructor to create a new promise and passed in the function with the resolve and reject parameters. We then used the setTimeout function to simulate an asynchronous task. After 1 second, we called the resolve function. This will "resolve" the promise.

Consuming/Handling a Promise

If we run this code, nothing will happen because we haven't dealt with the promise yet. To do that, we use the then method. The then method takes in a function that will be called when the promise is resolved. I do want to mention that there is an alternate way to handle promises and that is with something called Async/Await, which we will be learning in a little bit. For now, we will use the then method.

promise.then(function () {
  console.log('Promise consumed');
});

You also don't have to put the promise into a variable. You could just do this:

new Promise(function (resolve, reject) {
  // Do an async task
  setTimeout(function () {
    console.log('Async task complete');
    resolve();
  }, 1000);
}).then(function () {
  console.log('Promise consumed');
});

If we look in the console, we can see that the Async task complete message is logged first and then the Promise consumed message is logged. To show you that this is asynchronous, I will add a global console log to the bottom of the file.

console.log('Global console log');

Now if we run it, we will see the global console log first because the code is not blocked by the promise. The promise is asynchronous and non-blocking.

Returning Data from a Promise

To return data from a promise, we simply pass it in the resolve function. Let's say we want to return a user object from a promise. We can do that like this:

const promise = new Promise(function (resolve, reject) {
  // Do an async task
  setTimeout(function () {
    resolve({ name: 'John', age: 30 });
  }, 1000);
});

The then method takes in a function that has a parameter for the data that is returned from the promise. We can call it whatever we want, but I will call it user.

promise.then(function (user) {
  console.log(user);

Handling Errors

Remember, we also have a reject function that we can call when the promise is not successful, meaning there is some kind of error.

Let's create a variable that represents an error and then check for it and call the reject function if it exists. We will also pass in an error message.

const promise = new Promise(function (resolve, reject) {
  // Do an async task
  setTimeout(function () {
    let error = false;

    if (!error) {
      resolve({ name: 'John', age: 30 });
    } else {
      reject('Error: Something went wrong');
    }
  }, 1000);
});

If error is set to false, we get the same result as before. But let's set it to true and see what happens.

let error = true;

So, we do see our error message, but notice it also says Uncaught (in promise). This is because we are not handling the error. We can handle the error by using the catch method. The catch method takes in a function that has a parameter for the error message. We can call it whatever we want, but I will call it error.

promise
  .then(function (user) {
    console.log(user);
  })
  .catch(function (error) {
    console.log(error);
  });

Now, we are handling the error and we can see the error message in the console.

We can shorten this with arrow functions and implicit returns.

promise.then((user) => console.log(user)).catch((error) => console.log(error));

finally Method

The finally method is used to execute code after the promise is resolved or rejected. It will run no matter what. I personally have not had too many instances where I needed to add a finally block, but you should know it exists. Let's add a finally method to our promise.

promise
  .then((user) => {
    console.log(user);
    return user.name;
  })
  .then((name) => console.log(name))
  .catch((error) => console.log(error))
  .finally(() => console.log('The promise has been resolved or rejected'));
@TheOneAbove0
Copy link

woow

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment