Asynchronous Programming in JavaScript. Part II. Synchronous vs. Asynchronous code


JavaScript is at its most basic a synchronous, blocking, single-threaded language. That is the JavaScript engine can only process one statement at a time in a single thread.

The meaning of "single-threaded" has been covered in the first part of the article series. The current article concerns the meaning of two other words: "synchronous" and "blocking".

Synchronicity vs. Asynchronicity

The terms synchronous and asynchronous describe the how the code in the thread gets executed.

We can divide all JavaScript code into:

Whether we want to run code synchronously or asynchronously will depend on what we're trying to do.

There are times when we want things to load and happen right away but if we're running a computationally expensive operation like querying a database (which is usually implemented as asynchronous function provided by the Runtime Environment) and using the results to populate templates it is better to push this off the main thread and complete the task asynchronously.


REMINDER. In JavaScript asynchronous functions invoked the same way as synchronous function i.e. in the same order they are written in code:

console.log('a');
setTimeout(() => console.log('c'));
console.log('b');

First, console.log is invoked, next setTimeout and then another console.log.

The only difference between async and sync functions is how the function gets executed after its invocation (creation of its EC):

Synchronous code execution

Thread Blocking

Any JavaScript code that takes too long to return back control to the Event Loop will block the execution of any JavaScript code in the page, even block the UI thread, and the user cannot click around, scroll the page, and so on.

During synchronous execution, if the interpreter is busy processing some function's Execution Context, no other operation could be executed at the same moment. Moving to the next operation is also impossible until the current EC is fully processed. In other words, everything is blocked, until the current EC is executed.

Usually, the execution takes a split second and we don't notice any delay/freezing. But if this EC executes for too long (for example the synchronous request to a database or computationally intensive function) - we're in trouble: as I've said already, this EC blocks the execution of any JavaScript code on a page, even browser UI — it doesn't respond to any user actions: the user can't click, can't scroll the page, etc. Such a situation is called "thread blocking".

Example. Thread blocking when running slow synchronous function (for instance, the function performing computationally intensive tasks or synchronous request to database).

function computationHeavyTask() {
  // ...
}

computationHeavyTask(); 
// alternatively, here could have been a sync request to DB, 
// like databaseRequest();

consol.log('hi');

Let's reiterate: when the interpreter puts the Execution Context of computationHeavyTask synchronous function onto the Stack, it wouldn't be able to remove it until the function is fully executed. Thus, if the current EC can't be removed from the Stack, it is also impossible to add to the Stack the EC of console.log('hi'). We will see hi only when the computationHeavyTask() is 100% processed and its EC is removed from the Stack.

Coping with Thread Blocking

When you run the synchronous code, unfortunately there is no any way to avoid blocking the thread.

Nevertheless, it is possible to regain some control over the order of function execution using one standard approach to writing the synchronous code — сallbacks.

Callbacks are functions that are passed into other functions as arguments and are called when certain conditions occur. Callback functions can be named or anonymous functions.

Example. Using callback in forEach() method. Note that this callback is synchronous.

// the expected parameter of forEach() is a callback function, which itself
// takes two parameters: a reference to the array element and index values

const colors = ["red", "pink", "black", "yellow"];

colors.forEach((colorName, index) => {
  console.log(index + '. ' + colorName);
});

Example. We can also write the synchronous callback of our own.

function greeting(name) {
  console("Hi " + name);
}

function processUserInput(callback) {
  let name = prompt("Enter your name.");
  callback(name);
}

processUserInput(greeting);

That is, when we pass a callback function as a parameter to another function, we are only passing the function definition as the parameter — the callback function is not executed immediately. It is "called back" (hence the name) synchronously, somewhere inside the containing function's body. The containing function is responsible for executing the callback function when the time comes.

Callbacks allow not only to control the order in which functions run and data is passed between them, but they also allow you to pass data to different functions depending on circumstance. So you could have different actions to run on the user details you want to process like greeting(), goodbye(), addToDatabase(), requestEmailAddress(), etc.

However, for all their usefulness, such callbacks are still synchronous. They are still blocking the thread when they run.

Asynchronous code execution

NOTE. All the theory from this point and until the end of the article is concerned with "why do we need async code?" and "when should we use async code?".

We can execute the code asynchronously in a number of ways:

For the reasons illustrated earlier related to blocking (among many other reasons), many Web API features today use asynchronous code to run, especially those that influence or fetch some kind of resource from an external device, such as fetching a file from the network, accessing a database on the server and returning data from it, accessing a video stream from a web cam, etc.

Above I've listed three ways to code asynchronously, now I will show examples for each of them (except async/await cause there is nothing deserving of attention).

Asynchronous callback-functions

An example of an asynchronous callback is an event handler, i.e. function you pass as the second argument to addEventListener().

elem.addEventListener('click', () => {
  console.log('Hi!');
});

The first parameter is the type of event to be listened for, and the second one is a function that is invoked [asynchronousy] when the event is fired.

I'll remind right away: addEventListener is provided by Web API which in turn belongs to Browser Runtime Environment i.e. it is not a part of JavaScript, but a part of Browser Runtime Environment. addEventListener is asynchronous. It starts synchronously as any other async function and attaches the event handler to the element and is then removed from the Stack. When the event happens (we've clicked on some element), Web API creates a task "run the callback provided to addEventListener + pass it an event object with info about click as an argument" and puts the task into the Macrotask Queue. Then on some iteration of the Event Loop this task will be picked up from the Queue and executed.

What happens next is the standard behavior of the Event Loop (see the article about Event Loop for detail), here is a short explanation:

  1. After the JS Engine will finish processing the entire code of the script file, according to the Event Loop model, it will check the Microtask Queue and execute ALL tasks from this Queue
  2. Then the browser will render the page
  3. Then JS Engine will check Macrotask Queue again and invoke your event handler

Promise

Consider another approach to writing async code.

We retrieve data from API using fetch() method, which is asynchronous; it belongs to Web API.

Why is it asynchronous? Because Web API implementors decided to create it in that way.

console.log('1');

fetch('https://api.mixcloud.com/spartacus/')
  .then(response => {
    console.log(response);
    return response.json();
  })
  .then(user => console.log(user)); //

console.log('2');

The fetch() method is handled by Macrotask Queue (yes, mAcrotask), I won't explain here all the details of execution cause you should know them already from the article about Event Loop; if you don't understand something, consult the article about Event Loop.

Nevertheless, here is a brief explanation:

fetch is a part of Web API provided by Browser Runtime Environment. It is asynchronous, but it doesn't necessarily run in a separate thread, it depends on specific Runtime Environment and API implementation and may vary. We should not be concerned with this, the only thing to bear in mind is that it runs asynchronously.

  1. output 1
  2. after fetch() invocation and creation of its EC, its EC is instantly removed from the stack (so it does not block subsequent JavaScript code from running) and sync code under fetch(... continues to execute: in our case, the next sync code is console.log('2');.
  3. meanwhile our async method fetch continuous its execution in the background in a parallel thread or somehow else — it doesn't matter (it depends on specific RE and API implementation; explained above). After the fetch method got the file from the server, Fetch API generates a message into a Macrotask Queue and associates the callback function we gave to then with this message.
  4. after the main thread (i.e. the first task/message from Macrotask Queue) has finished processing, according to the Event Loop model, JS Engine checks Microtask Queue: it is empty, so rendering of the page happens. Then, JS Engine checks Macrotask Queue again and sees there is a message from Fetch API. Then, it checks the stack and sees it is empty. So it starts processing the Queue: it takes the message with the associated callback function (the callback we passed to then), takes this function, and passes it to call stack, so the function is invoked and runs.

Mixing sync and async code

The content of this section is taken from MDN.

Let's explore what happens when we try mixing sync and async code, so we can further understand the difference. The following example is fairly similar to what we've seen before.

console.log ('Starting');
let image;

fetch('coffee.jpg').then((response) => {
  console.log('It worked :)')
  return response.blob();
}).then((myBlob) => {
  let objectURL = URL.createObjectURL(myBlob);
  image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}).catch((error) => {
  console.log('There has been a problem with your fetch operation: ' + error.message);
});

console.log ('All done!');
  1. The browser will start executing the code, see the first console.log() statement (Starting) and execute it, and then initialize the image variable.
  2. It will then move to the next line and begin executing the fetch() block but, because it is async and not blocking, it will move on with the code execution, finding the last console.log statement (All done!) and outputting it to the console.
  3. Only once the fetch() block has completely finished running and delivering its result through the .then() blocks will we finally see the second console.log() message (It worked ;)) appear. So the messages have appeared in a different order to what you'd expect:
    1. Starting
    2. All done!
    3. It worked :)

In a less trivial code example, this could cause a problem — you can't include an async code block that returns a result, which you then rely on later in a sync code block. You just can't guarantee that the async function will return before the browser has processed the async block.

To see this in action, run the code shown above but change the third console.log() call - console.log ('All done!'); to the following:

console.log ('All done! ' + image + 'displayed.');

You should now get an error in your console instead of the third message: [there is no error; looks like it is a mistake in MDN article; instead of an error shown below, the output is StartingAll done! undefineddisplayed.It worked :)]

TypeError: image is undefined; can't access its "src" property

This is because at the time the browser tries to run the third console.log() statement, the fetch() block has not finished running so the image variable has not been given a value [but even if it would have finished running, the result would have been the same cause the message from Fetch API is handled only after all sync code in the script (recall: all code inside a file wrapped in a single anonymous function and is handled by Macrotask Queue as a single task, which must be 100% complete before handling Microtask Queue -> rendering -> and then handling 1 task from Macrotask Queue again)].

To fix the problematic fetch() example described above and make the three console.log() statements appear in the desired order, you could make the third console.log() statement run async as well.

This can be done by moving it inside another .then() block chained onto the end of the second one, or by moving it inside the second then() block. Try fixing this now. Here is an example of how it should be done.

Glossary

Q&A

TODO: move the bullet points below to another article.

References

Further Reading