DEV Community

Cover image for All you need to know about asynchronous JavaScript
Pippa Thompson
Pippa Thompson

Posted on

All you need to know about asynchronous JavaScript

If you have ever spent time in the JavaScript world you will probably have heard the phrase "JavaScript is a single threaded language". But what does that mean exactly? Put simply, it means that JavaScript executes code line by line in sequential order, and after each line has been executed there's not going back to it.

So what's the problem?

Imagine a situation where a user wants to display a list of comments on a website. They click a button to view the comments. However, while the comments are being fetched from a server (and let's say that the fetching takes a while), the user cannot interact with anything else on the website because JavaScript is busy executing the show comments functionality. In other words, all other code is blocked until JavaScript has completed this comment fetching task. Not very user friendly.

const name = Pippa;
const number = 100_000_000;
for (let i = 0; i <= number; i++) {
  console.log(i)    
}

console.log(`Hi, my name is ${name}`)
Enter fullscreen mode Exit fullscreen mode

In this trivial example, a time consuming command (in this case, iterating through a large for loop) would block the final console.log() command for an undetermined amount of time. Nothing else can happen in the program while the for loop is running, and who knows how long that could take to complete.

The solution: enter asynchronous JavaScript.

Async JS is a technique (or rather, group of techniques) which enables your program to continue executing code while time consuming tasks are deferred. No, it's not magic, it is just a set of features of both JavaScript and the browser which when combined allow us to write non-blocking code. Let's start with the first, and probably most simple, method.

Callbacks: using the setTimeout() API

If you have read up on the JavaScript runtime environment (and if you've haven't you can do so here) this one should come as no surprise. The browser provides the event loop, the callback queue and the microtask queue. These features allow certain functionality to be deferred until all global code has been executed. setTimeout() (a browser API) takes a callback function and a time argument (minimum time in milliseconds to pass before invoking the callback) and pushes the callback function to the callback queue. The callback queue must wait for all other global code to run before anything in it can be pushed to the call stack (this is handled by the event loop), thus we can essentially call this function knowing that our global code will continue executing in the meantime.

console.log('first hello');

function fetchData() {
  setTimeout(() => {
    // functionality to make XHR request to fetch data from a server and log it to the console
  }, 1000)
}

fetchData();

console.log('second hello ')

// OUTPUT --> 
// 'first hello '
// 'second hello'
// '[ object Response ] {...}'
Enter fullscreen mode Exit fullscreen mode

In the above example we start with console.log('first hello'). We then declare the function fetchData(). This function emulates making an XML/HTTP request, which is wrapped in the setTimeout() browser API. Once we call the fetchData() function, the setTimeout() starts a timer up in the browser with a time of 1000ms. The callback function wrapped inside the setTimeout() is sent to the callback queue where it will be waiting for at least 1000ms. Meanwhile JavaScript continues on to the second console.log('second hello'). Once the second console.log() is logged to the console and the callstack is clear of all global code, the fetchData() callback function is able to give us the response it received from the server. No code has been blocked and our data has been fetched!

Promises and the fetch() API

In this section we will look at promises in the context of using the fetch() API. It’s worth noting, however, this is not the only use case for promises. There's loads of great resources on promises out there such as this article.

With ES6 came the introduction of promises. In order to understand how they facilitate asynchronous code we need to take a closer look at their features and behaviour. Promises are just special objects that return a placeholder object until some asynchronous code has been resolved. Every promise has several properties including state, value, onFulfilled and onRejected.

  • State: represents current status of the promise object which can be one of three options: pending (the default), resolved or rejected.
  • Value: this is undefined by default, but will eventually evaluate to the response of the asynchronous task.
  • onFulfilled: an array containing functions to be run once the value property has been successfully resolved. We push functions into this array by using the .then() method. The callbacks in this array receive an argument which is the response of the fetch() call they are chained on to.
  • onRejected: an array containing functions to run if the promise is rejected, so essentially error handling functions. We push functions into this array by using the .catch() method. The callbacks in this array receive an argument which is the error received in the fetch() call they are chained on to.

Promises allowed for the creation of the modern XHR browser function known as fetch(). I would like to stress that that fetch() is a a feature of the browser, so it is not native to JavaScript even though it looks like a regular function. Once invoked, fetch has two main tasks; one is to return a placeholder object (a promise) and the other is to send an XML/HTTP request from the browser to a specified url.

Here’s the big reveal - any promise deferred functionality is sent to the microtask queue (separate from the callback queue, which has lower priority). The functionality in the microtask queue must wait until all global code has been executed until it can be pushed to the call stack. Thus, we can:

  • Run an asynchronous operation, i.e. fetching data from a server.
  • Execute all other global code meanwhile.
  • Trigger response related functionality from the onFulfilled array once all global code has been run and we’ve received a response from an asynchronous operation. Alternatively, if the promise rejects we can trigger error handling functionality from the onRejected array.

console.log('me first');

function fetchData() {
  fetch('https://dog.ceo/api/breeds/image/random')
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(error => console.log(error))
}

fetchData();

console.log('me second')

// OUTPUT --> 
// 'me first'
// 'me second'
// '[ object Object ] {...}'

Enter fullscreen mode Exit fullscreen mode

And there you have it, asynchronous code which allows a more time intensive command to be run (in this case, the XHR functionality) without blocking the rest of the JavaScript code in the file.

Side note: there is also another promise array, similar to onFulfilled/onRejected, which allows us to run callbacks once the promise has been settled (either resolved or rejected). We push callbacks into this array using the .finally() method and it's useful for performing any clean up tasks.

Generator Functions and Async/Await

Generator functions are an ES6 feature of JavaScript. Up until now, we understand that once a line of JavaScript code has been executed it is gone for good - there’s no going back up to that line. However, the introduction of generator functions has changed this model. Generators are special types of functions marked with an asterisk. They use a very powerful keyword, yield. I like to think of the yield keyword as the return keyword with superpowers. Yield allows us to pause a generator function’s execution context, return out a value, but later re-enter that same function execution context! We re-enter the generator funtion's execution context by accessing the .next() method on the generator function. This can be quite tricky to wrap your head around, so let me show you an example:


function* myNumberGenerator() {
        yield 1
        yield 2
        yield 3
}

const returnNextNumber = myNumberGenerator();

const number1 = returnNextNumber.next().value; 
const number2 = returnNextNumber.next().value; 
const number3 = returnNextNumber.next().value;

console.log(number1);
console.log(number2);
console.log(number3);

// OUTPUT -->
// 1
// 2
// 3

Enter fullscreen mode Exit fullscreen mode

On line 9 we are assigning the value of number1 to returnNextNumber.next().value. We established that .next() allows us to re-enter the function execution context, but we have not yet seen the .value syntax.
When we are kicked out of the generator function by the yield keyword, we know that we also receive a return value. However, what we actually get is an object with two properties; done (a boolean) and the value itself (in this case a number). In order to access just the value itself, we must use .value.
On line 10, we assign the variable number2 to the result of calling returnNextNumber.next().value, so number2 evaluates to 2. This is because we re-entered the generator function where we left off and once more encounter the yield keyword, this time with the value 2. The same pattern continues on line 11.

We know that once a normal function has been invoked, its execution context is automatically garbage collected, but here we are able to go back into the function and continue running it later on in the code. Cool, right? Let’s take a look at another example:


function* aGeneratorFunc() {
    const number = 10;
    const newNumber = yield number;
    yield 10 + newNumber;
    yield 20;
}

const returnNextElement = aGeneratorFunc();
const element1 = returnNextElement.next().value; // 10
const element2 = returnNextElement.next(2).value; // 12

// OUTPUT --> 
// 10
// 12

Enter fullscreen mode Exit fullscreen mode

Wait, what… the number 12 was returned and not 20? Why? Because not only can we re-enter a function, we can re-enter the function and bring some new data with us.

The first time we call returnNextElement.next().value on line 9, yield number (line 3) threw us out of the function before it had time to assign the value of number to the variable newNumber. So, at this point newNumber is undefined. The next time we call returnNextElement.next(2).value (line 10) with 2 as an argument, we re-enter the function where we left off last time, i.e. on line 3 where we were just about to assign the value of newNumber. The argument we passed in, the number 2, is inserted where we left off, so newNumber evaluates to 2. We move to the next line yield 10 + newNumber, and we are thrown out of the function execution context along with the evaluation of 10 + newNumber, which as we now know is 2. Hence, we return out 12. If we were to re-enter the function once more and save returnNextElement.next().value to the variable element3, the value of element3 would be 20.

So now we understand how it’s possible to re-enter an execution context of a function, with additional data if we wish, but how does this link to async JS?

Introducing the ES7 feature async/await.

Async/Await is made possible due to generators (and promises). With this ability to re-enter a function later on in our code, we can start executing a more time consuming command (such as fetching data), continue on with the global code, and then re-enter the data-fetching function once the requested data has been returned and all global code has been run.

Async/await has a clean and simple syntax which requires you to place the async keyword before the function definition. The await keyword is used within the function scope, and commands JavaScript to pause the function execution until the result of the promise has been returned and continue on with the rest of the program code. After the global code has been run, JavaScript will return back into the function where it left off, by this time with a received response, and continue with the rest of the logic. Again, let’s take a look at an example:


async function getData() {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
  const data = await response.json();
  console.log(data);
}

getData();

console.log('me first!');

// OUTPUT →
// 'me first!'
// [ object Promise ] {...}

Enter fullscreen mode Exit fullscreen mode

Let’s break it down:

  • We declare an async function using the async keyword.
  • We use the await keyword to signal that a promise will be returned from the fetch call which enables us to break out of the function and return to it once the promise has been settled and global code has been run.
  • We call the getData() function, but the console.log() runs first because the fetching of API data has been deferred to the microtask queue.
  • JavaScript continues running the global code, then re-enters the getData() function once all global code has run and the response is available in getData(). It then continues executing the rest of the getData() function.

A note on error handling in async/await. In order to facilitate error handling in an async function, you can wrap the functionality in a try/catch statement. This gives you the option to run some code upon promise failure, essentially the same as using the .catch() method with fetch(). Using the same example we just saw, let’s see what it looks like using a try/catch statement:


async function getData(){

  try {
        const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
        const data = await response.json();
        console.log(data);
       } catch (error) {
        console.log(error)
    }
}

getData();

console.log('me first');


// OUTPUT→
// 'me first!'
// [ object Promise ] {...}

// if an error occurs when fetching the data, the output would be:
// ‘me first!’
// [ object Error ] {...}

Enter fullscreen mode Exit fullscreen mode

And there you have it, asynchronous JavaScript. If you made it this far, thanks for reading! If you want to dive deeper into the topic there are loads of great resources out there, but I thoroughly recommend watching Will Sentence’s Hard Parts video courses available on Front End Masters.

Top comments (3)

Collapse
 
muneerahmedkhan profile image
Muneer Khan

nice explanation to async in javascript

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍

Collapse
 
ppiippaa profile image
Pippa Thompson

Thank you so much!