The JavaScript Runtime

Overview on how Asynchronous JavaScript is Processed

What is the difference between asynchronous and synchronous JavaScript? JavaScript is a single-threaded language that can be non-blocking, but what does that mean? These are some of the questions that will be answered here today. But first let’s start from the beginning. When a program is run, it needs to:

  1. allocate memory and
  2. parse and execute.

This is handled by the JavaScript Engine, which consists of the Memory Heap and a Call Stack. For simplicity, let’s say the JavaScript Engine looks something like the image above. The Memory Heap is where memory allocation happens. You can allocate memory simply by declaring a variable.

// Memory Heap
const a = 1;
const b = 10;
const c = 100;

But the Memory Heap is limited in size. If you fill the box to capacity and continue to try to allocate memory, you get what is known as a Memory Leak.

The other part of the JavaScript Engine is the Call Stack, which is where your stack frame lives as your code executes. A stack is a data structure that you can think of as a stack of plates; you can add to the top of the stack, and you can take from the top of the stack. To add to the call stack, you can simply call some functions.

// Call Stack
console.log('1');
console.log('2');
console.log('3');

In the above example each line gets added to the stack, is executed, and is removed from stack before the next line. This can get a little more complex with nested functions though.

// More complicated call stack
const one = () => {
  const two = () => {
    console.log('4');
  }
  two();
}
one();

The stack gets executed as follows:

  1. one() gets added to the call stack
  2. two() gets added to the call stack
  3. console.log(‘4’) gets added to the call stack
  4. console.log(‘4’) executes and is removed from the call stack
  5. two() finishes executing and is removed from the call stack
  6. one() finally finishes executing and is removed from the call stack

Now suppose you have a function foo(), which does nothing but call itself.

function foo() {
  foo();
}
foo();

This is an example of recursion. Like the memory heap, space for the call stack is limited. If you overflow the call stack, you get what’s known as stack overflow.

Let’s revisit: JavaScript is a single threaded language that can be non-blocking. What does that mean? It simply means that JavaScript only has one call stack. Some programming languages, such as C, C++, Java, and more can be multithreaded, or have multiple call stacks, but that leads to other problems, such as deadlocks, which is when two or more threads are blocked forever waiting on each other.

Back to the other question: wWat is synchronous programming? It just means code is executed sequentially, in order. There are problems with single threaded synchronous programming languages like JavaScript though. What if we have to do another task? If we sit there and wait for the blocking task to finish, that would be a bad user experience. We need something non-blocking!

Asynchronous to the rescue! Synchronous programming is great because it’s predictable, but it also gets really slow if we have to wait for each task to finish before starting another when we could potentially have both tasks running parallel. Asynchronous programming is needed for tasks that can benefit from parallel processing such as image processing and network requests (API calls). Below is an example of asynchronous code. Can you guess the order of execution and what will be printed to the console if this code is run?

console.log('1');
setTimeout(() => {
  console.log('2');
}, 2000);
console.log('3');

Why is the output in that order? JavaScript itself is not asynchronous and the JavaScript engine itself cannot execute the code above, which is why we need a JavaScript Runtime Environment. DOM manipulation, AJAX requests, and setTimeout are not part of JavaScript itself, they are part of the Web APIs. Let’s break down this asynchronous example:

  1. console.log(‘1’) gets added to the call stack, then executed
  2. setTimeout() gets called by the Web API
  3. Meanwhile, console.log(‘3’) gets added to the call stack and executed
  4. While this is happening, setTimeout() is counting down 2000 milliseconds, or 2 seconds
  5. When it is done, the task gets sent to the callback queue (Queue is another common Data Structure in Computer Science). It passes tasks one by one to the event loop.
  6. The event loop is constantly checking: “Is the call stack empty?” When it finally is empty, console.log(‘2’) finally gets called and executed. Hence the reason the output of the code is 1, 3, 2.

As you can see, the a JavaScript Engine is not enough to execute that code. You need a JavaScript Runtime Environment! Knowing what you know now, let’s test your understanding. Let’s say we chance the 2000 milliseconds (2 seconds) to 0 milliseconds. What would the ouput be?

console.log('1');
setTimeout(() => {
  console.log('2');
}, 0);
console.log('3');

Although setTimeout() is set for 0 milliseconds, it still has to go through the entire process of sending the request to the Web APIs, going through the call back queue, then back onto the call stack. Here is a visual example of asynchronous code being executed in the JavaScript Runtime.

Let’s examine a more complex example. Can you guess the output order?

setTimeout(() => {
  console.log('1', 'Welcome to the jungle');
}, 0);

setTimeout(() => {
  console.log('2', 'We got fun and games');
}, 10);

Promise.resolve('We got everything you want').then(data => {
  console.log('3', data);
});

console.log('4', 'Honey we know the names');

Wait! What? But you said… Yes I know what I said. The thing is, JavaScript changes over time, and new features are constantly being added, one of which is a native way to process asynchronous code, AKA promises. Before promises, asynchronous code was handled by callbacks. Unfortunately callbacks would get nested with other callbacks, which were nested with other callbacks. You were left with what was termed “callback hell.” The result was unelegant, repetitive code.

Take a look at this code:

movePlayer(100, 'Left', function() {
  movePlayer(400, 'Left', function() {
    movePlayer(10, 'Right', function() {
      movePlayer(330, 'Left', function() {
      });
    });
  });
});

With promises, we have a much more elegant way to handle asynchronous code. The same code above can be rewritten with promises like this:

movePlayer(100, 'Left')
  .then(() => movePlayer(400, 'Left'))
  .then(() => movePlayer(10, 'Right'))
  .then(() => movePlayer(330, 'Left'))

ES6 Runtime

With ES6 and beyond, the JavaScript Runtime Environment now comes with what is known as the Job Queue, which has priority over the Callback Queue. In other words, anything sent to the Job Queue will get processed by the Event Loop before anything that is sent to the Callback Queue. Therefore, native ES6 promises, which are sent to the Job Queue, get processed and executed before anything that is sent to the the Callback Queue via Web API’s (which is where setTimeout() is sent).