Synchronicity & Asynchronicity in JavaScript

In the programming universe, there are usually two programming models that are thrown around that define how software is designed and executed.

These are synchronous programming and asynchronous programming, or sync and async for short.

In JavaScript, code is executed line-by-line synchronously to the end but with advancements in web browsers through ECMAScript libraries, asynchronous programming can be achieved.

Down To Basics

Let’s go through some basics on how JavaScript operates in the context of web browsers.

  • JavaScript is a single-threaded language which means only one instance of code can be run at any given time blocking other code execution.

  • JavaScript is, by design, a synchronous language where code lines run in sequence, line-by-line, to the end.

  • Code that is currently running is part of the Global Execution Context

  • Variables and functions that are declared and stored are part of the Global Variable Environment.

  • When functions are run, that function block creates a Local Execution Context, or Scope, for this subset of code to run along with its own Local Variable Environment.

  • The Call Stack is a LIFO data structure which demonstrates the ordering of contexts for JavaScript to execute code in.

    By default, the Global Execution Context is part of the Call Stack and any functions that are called will have their Local Execution Contexts added to the Call Stack and then popped out of the Call Stack when that function is complete. Afterwards, the Global Execution Context resumes its execution to the end.

  • The mechanism that describes this sequencing of events is known as the Event Loop. This mechanism waits for the Call Stack to complete ongoing functions in order to add in more functions once it is clear.

Synchronicity in JavaScript

RelayRace

Imagine 4 kids running in a relay race on a track field split up into four 100 m sections. Each kid is split up starting at the 0m mark and each 100m interval towards the end. Each kid must wait to begin running until they receive a baton from the previous kid that was running.

The first kid begins running and while they are running, each kid is blocked from continuing the race until they receive the baton from the kid before them who just finished their section of the race.

Let’s say the third kid trips while running and takes a bit of time to start running again. During this time, the fourth kid who is still waiting to complete the race is now also delayed until the third kid gets back up and continues their section of the race.

Similarly, this is the manner in which synchronous code is executed.

Synchronous Line-By-Line

Synchronous code that will declare a variable and function and print out the variable and execute the function to print out a string.

const num = 1;

function printHello() {
  console.log('Hello');
}

console.log('num', num);

printHello();
Prints
  • “num” 1
  • “Hello”
Explanation
  • Code is running in the Global Execution Context
  • Variable num is declared to our Global Variable Environment
  • Function printHello is declared in our Global Variable Environment
  • The value of num is printed on the Browser’s console
  • printHello is called creating a Local Execution Context that is added to the Call Stack
  • The string Hello is printed on the Browser’s console

Here we see each line run and interpreted how the Browser would orchestrate these JavaScript lines.

For simplicity sake, let’s assume each line takes 1 millisecond to execute.

Synchronous For Loop Blocking

Synchronous code that will declare a function, print before and after a loop that takes some time and then executes that function to print out a string.

function printHello() {
  console.log('Hello');
}

console.log('before loop');

for(let i = 0; i < 500; i++) {
  console.log(i + 1);
}

console.log('after loop');

printHello();
Prints
  • “before loop”
  • Series of iterated numbers from 1 to 500
  • “after loop”
  • “Hello”
Explanation
  • Code is running in the Global Execution Context
  • Function printHello is declared in our Global Variable Environment
  • The message “before loop” is printed to the Browser’s console
  • Loop through and print an iteration from 1 to 500
  • The message “after loop” is printed to the Browser’s console
  • printHello is called creating a Local Execution Context that is added to the Call Stack
  • The string “Hello” is printed on the Browser’s console

This for loop does an operation for some time, let’s say 500 milliseconds, during which the rest of the code in the Global Execution Context is blocked until this loop is completed.

Issue with Synchronicity

In the above example, the code runs as expected where the for loop blocks execution for the lower lines of code. However, let’s say we want to execute printHello while some long-running operation is happening in the background.

JavaScript wouldn’t be able to accomplish this itself since JavaScript is single-threaded and therefore, two operations, the global context and the background operation, cannot run at the same time.

So here is where the concept of Asynchronicity comes into play.

Asynchronicity in JavaScript

Cooking

Imagine a 5 star restaurant on a busy Friday night, full of customers coming in with orders for small and large groups.

The chef staff has to cook and serve each guest’s meal in a short amount of time to safely secure their 5 star rating.

So ideally, the chefs will split up parts of the meal which can all be completed independently from each other. In addition to this, they could also group up sections of each meal from multiple guests to reduce time between completing one meal.

Thus, each meal will not be blocked or waiting too long for a section to be completed before being fully ready to be served out to guests.

Also, with enough chefs in the kitchen, the timing can be reduced further by splitting up tasks between team members.

Similarly, in asynchronous programming, whenever logic needs to be retrieved from a server or deferred while some other logic is happening, we rely on other execution threads to be running while the JavaScript thread is in execution.

In this case, there are Asynchronous Web Browser APIs that are implemented by each browser and their functionality is dictated by and kept consistent between each other by the W3C.

Web Browser APIs span several different domains of logic, such as DOM API, Timer API, Fetch API, Promises API, Canvas API and more. In terms of Asynchronous Web Browser APIs, let’s focus on Timer API and Fetch / Promises API.

When these external functions in these library APIs are called, their executions are performed on node background tasks which gives JavaScript a form of pseudo-multi-threaded performance to accomplish asynchronous operations in its single-threaded logic.

On the JavaScript code side, these functions are usually called facade functions.

Asynchronous with setTimeout

Asynchronous code that will declare a variable and function, execute setTimeout and print the variable and function.

const num = 1;

function printHello() {
  console.log('Hello');
}

setTimeout(printHello, 0);

console.log('num', num);

Prints:

  • “num” 1
  • “Hello”

Explanation

  • Code is running in the Global Execution Context
  • Variable num is declared to our Global Variable Environment
  • Function printHello is declared in our Global Variable Environment
  • setTimeout is a facade function in JavaScript which calls the Timer API on the Browser side to execute some form of timing
  • On the JavaScript side, the setTimeout line is complete and moves forward.
  • The value of num is printed on the Browser’s console
  • On the Browser side, the Timer which finishes immediately, sends the function provided to the MacroTask Queue
  • Once the Global Execution Context code is complete, the function in the MacroTask Queue is moved and executed by the Call Stack
  • printHello is called creating a Local Execution Context that is added to the Call Stack
  • The string “Hello” is printed on the Browser’s console

MacroTask Queue (Callback Queue) - FIFO

In this above example, reading line by line, the expectation would be that the “Hello” gets printed out before num, however, JavaScript has a queue called the MacroTask Queue where deferred functions that will provide a result later are placed.

Therefore, the setTimeout, although it has a timing of 0 seconds and happens immediately, is deferred.

When the Browser Timer API finishes, the function passed to it, printHello, is placed in a MacroTask Queue and has to wait for the Global Execution Context to finish executing its code in order for this function to leave the MacroTask Queue and enter the Call Stack to be executed.

Asynchronous with setTimeout and Promise

Asynchronous code that will alert on a Timer, a Promise and in the Global Execution Context

setTimeout(() => alert('timeout'));

Promise.resolve()
  .then(() => alert('promise'));

alert('code');

Prints

  • Alerted “code”
  • Alerted “promise”
  • Alerted “timeout”

Explanation

  • Code is running in the Global Execution Context
  • setTimeout is a facade function and its functionality is handled by Timer API
  • Code reaches Promise.resolve()
  • Promises are facade functions and the logic is setup with the Promises API
  • Code moves forward and the string “code” is alerted
  • During this, on the Browser API, the setTimeout finishes immediately and its function is added to the MacroTask Queue and the Promise is resolved immediately and its function is added to the MicroTask Queue
  • With no code running in the Global Execution Context, the function in the MicroTask Queue is moved to the Call Stack to be executed and the string “promise” is alerted
  • With no code running in the Global Execution Context and no resolved functions in the MicroTask Queue, the function in the MacroTask Queue is moved to the Call Stack to be executed and the string “timeout” is alerted.

MicroTask Queue - FIFO

In this above example, the Global Execution Context, MacroTask Queue and the MicroTask Queue are working together to set up the ordering of functions to be executed in the Call Stack.

The prioritization of functions is handled by the Event Loop and the MicroTask Queue takes higher priority than the MacroTask Queue which is why although, both queues had functions awaiting to be run after the Global Execution Context finishes, the Event Loop dictates that the MicroTask Queue with its resolved functions runs first over the MacroTask Queue.

Since the MicroTask Queue relies on functions to be resolved in order to be added to it while this is happening, the MacroTask Queue or the Global Execution Context can continue their functionalities.

Summary Rules of MacroTask & MicroTask Queues

MacroTask Queue runs its functions only if:

  • There is no code currently running in the Global Execution Context
  • There is no resolved functions in the MicroTask Queue

MicroTask Queue runs its functions only if:

  • There is no code currently running in the Global Execution Context

Asynchronous with setTimeout and Fetch API

Asynchronous code that handles a timeout, fetches and prints a server response.

const num = 1;

function printHello() {
  console.log('Hello');
}

setTimeout(printHello, 5000);

const getDog = fetch('https://dog.ceo/api/breeds/image/random');

getDog
  .then((response) => response.json())
  .then((result) => console.log('doggie', result));

setTimeout(printHello, 0);

console.log('num', num);

Prints

  • “num” 1
  • “Hello” with 0 seconds
  • “doggie” with the fetch result
  • “Hello” with 5 seconds

Explanation:

  • Code is running in the Global Execution Context
  • setTimeout is a facade function and its functionality is handled by Timer API
  • Code reaches fetch which, similarly to Promise, is a facade function and is handled by the Fetch API
  • During this, on the Browser API, the setTimeout finishes immediately and its function is added to the MacroTask Queue and the fetch is pending.
  • On the JavaScript side, we go forward towards the next setTimeout which is setup on the Browser API
  • Code moves forward and prints the string “num” with the value for num
  • With no code running in the Global Execution Context, the timeout for the 0 seconds happens and prints “Hello” since the fetch has not been resolved yet, allowing the MacroTask Queue to put its function in the Call Stack
  • When the Promise is resolved, the MicroTask Queue puts its function in the Call Stack allowing the message “doggie” along with the response from the server to be printed out
  • Afterwards, the last remaining timeout function is printed out after the 5 seconds to print “Hello”

Asynchronous with setTimeout and Promises with Async / Await

Asynchronous code that handles a timeout, async / awaits a Promise and prints a server response.

const num = 1;

function printHello() {
  console.log('Hello');
}

setTimeout(printHello, 5000);

async function getDog() {
  console.log('in function');
  const response = await Promise.resolve('result');
  console.log('doggie', response);
  return response;
}

getDog();

setTimeout(printHello, 0);

console.log('num', num);

Prints

  • “in function”
  • “num” 1
  • “doggie” with promise response
  • “Hello” with 0 seconds
  • “Hello” with 5 seconds

Explanation

  • Code is running in the Global Execution Context
  • setTimeout is a facade function and its functionality is handled by Timer API
  • Code reaches getDog()
  • Within getDog, the message “in function” is printed
  • Since getDog is an async function when there is an await for a deferred function, that function is momentarily suspended to continue the Global Execution Context
  • Code moves forward and the string “num” is printed with the value of num
  • During this, on the Browser API, the setTimeout finishes immediately and its function is added to the MacroTask Queue and the Promise.resolve() is awaiting to be run which resolves immediately and is placed in the MicroTask Queue
  • With no code running in the Global Execution Context, the MicroTask Queue finishes up the awaited Promise and prints the “doggie” with the response
  • After the MicroTask Queue is empty, the MacroTask Queue is allowed to execute their functions for the timeout of 0 seconds and timeout of 5 seconds.

Async / Await in JavaScript

When an async / await, the function is momentarily suspended until the function being awaited is actually resolved or completed.

In the above example, since we awaited the Promise, we momentarily leave the Local Execution Context and go back to the Global Execution Context to finish the code execution there.

Afterwards, the Promise.resolve() is free to be added to the Call Stack from the MicroTask Queue and continue the regular execution.

Asynchronous with setTimeout and Fetch with Async / Await Example

Asynchronous code that handles a timeout, async / awaits a Promise and prints a server response.

const num = 1;

function printHello() {
  console.log('Hello');
}

setTimeout(printHello, 5000);

async function getDog() {
  console.log('in function');
  const response = await fetch('https://dog.ceo/api/breeds/image/random');
  const result = await response.json();
  console.log('doggie', result);
  return result;
}

getDog();

setTimeout(printHello, 0);

console.log('num', num);

Prints

  • “in function”
  • “num” 1
  • “Hello” with 0 seconds
  • “doggie” with promise result
  • “Hello” with 5 seconds

Explanation

  • Code is running in the Global Execution Context
  • setTimeout is a facade function and its functionality is handled by Timer API
  • Code reaches getDog()
  • Within getDog, the message “in function” is printed
  • Since getDog is an async function when there is an await for a deferred function, that function is momentarily suspended to continue the Global Execution Context
  • Code moves forward and the string “num” is printed with the value of num
  • During this, on the Browser API, the setTimeout finishes immediately and its function is added to the MacroTask Queue and the fetch is awaiting for the response from the external API
  • With no code running in the Global Execution Context and no resolved functions in the MicroTask Queue, the timeout in the MacroTask Queue for 0 seconds is free to be executed in the Call Stack
  • When the fetch resolves, the resolved function is added to the MicroTask Queue and then is executed in the Call Stack printing “doggie” with the server response
  • After the MicroTask Queue is empty, the MacroTask Queue is allowed to execute the remaining timeout function of 5 seconds

Summary

Summary

JavaScript is synchronous and handles asynchronous behavior through different libraries provided by Browsers or other Node libraries.

The ordering of which functions get added to the Call Stack is handled by the Event Loop and the Global Execution Context is always at the bottom of the Call Stack.

The MacroTask Queue waits for no code to be running in the Global Execution Context and no waiting resolved functions in a MicroTask Queue.

The MicroTask Queue waits for no code to be running in the Global Execution Context.

Async / Await functionality gives a synchronous look and feel to JavaScript to momentarily suspend a function to return to it once the awaited functionality is resolved or completed.

References