The original address

Ever wonder how browsers read and run JavaScript code? It looks amazing, but you can get some clues about what’s going on behind the scenes.

Let’s immerse ourselves in the language of the JavaScript engine by introducing it to its wonderful world.

Open the browser console in Chrome and look at the “Sources” TAB. You’ll see some boxes, one of the interesting ones is called the Call Stack (in Firefox, you can insert a breakpoint into your code and see the Call Stack) :

What is a call stack? There seems to be a lot to talk about, even if it runs a few lines of code. In fact, JavaScript is not available out of the box for Web browsers.

Compiling and interpreting JavaScript code is an important part, and that’s the JavaScript engine. The most popular JavaScript engines are V8, Google Chrome, and Node.js, SpiderMonkey’s Firefox, and JavaScriptCore’s Safari/WebKit.

Today, avaScript engines are great projects that don’t cover every aspect of them. There are small parts of what every engine does that are difficult for us.

One of these components is the call stack, which along with global memory and execution context makes it possible to run our code. Ready to learn about them?

  • directory
    • JavaScript engine and global memory
    • Global execution context and call stack
    • Single threaded JavaScript and other interesting stories
    • Asynchronous JavaScript, callback queues, and event loops
    • Call back to Hell and ES6 Promises
    • Create and use JavaScript Promises
    • Mistakes in ES6 Promises
    • ES6 Promises combination: Promise. All, Promise. AllSettled, Promise
    • ES6 Promises and MicroTask Queues
    • JavaScript Engines: How do they work? Async /await: Promises to Async /await
    • conclusion

directory

JavaScript engine and global memory

I mentioned that JavaScript is both a compiled language and an interpreted language. Believe it or not, the JavaScript engine actually compiles your code with only a few subtleties before executing it.

Does that sound magical? It is called JIT (Just-in-time compilation). This is a big topic in itself, and another book is not enough to describe how JIT works. But now we can skip the theory behind compilation and focus on the execution phase, which doesn’t make it any less fun.

Consider the following code first:

var num = 2;
function pow(num) {
    return num * num;
}
Copy the code

If I asked you how do you handle the above code in the browser? What would you say? You might say “browser reads code” or “browser executes code.”

The reality is more nuanced than that. First, the browser is not reading the code snippet. This is the engine. The JavaScript engine reads the code, and as soon as it hits the first line, it puts some references into global memory.

** Global memory (also called Heap) ** is the area where the JavaScript engine holds variable and function declarations. So, going back to our example, when the engine reads the code above, global memory is filled with two bindings:

var num = 2;
function pow(num) {
    return num * num;
}
pow(num);
Copy the code

What will happen? Now things get interesting. When a function is called, the JavaScript engine makes room for two other boxes:

  • Global execution context
  • The call stack

Let’s see what they mean in the next section.

Global execution context and call stack

You learned how the JavaScript engine reads variable and function declarations. They end up in global memory (Heap).

But now that we’ve executed a JavaScript function, the engine has to handle it. How to do? Every JavaScript engine has a basic component called the call stack.

The call stack is a stack data structure: this means that elements can enter from the top, but if they have some elements above them, they can’t leave. That’s what JavaScript functions look like.

Once executed, you cannot leave the call stack if some other function remains stuck. Note that keeping “JavaScript is single-threaded” in mind helps to understand this concept.

But now let’s go back to our example. When the function is called, the engine pushes the function in the call stack:

I like to think of the Call Stack as a bucket of potato chips. If you don’t eat the top chips first, you can’t eat the bottom chips! Fortunately our functionality is synchronous: it’s a simple multiplication, and it can be computed quickly.

At the same time, the engine also assigns a global execution context, which is the global environment in which we run our JavaScript code. This is what it looks like:

Imagine a global execution context as an ocean in which JavaScript global functions swim like fish. So wonderful! But that’s only half the story. What if our function has some nested variables or one or more internal functions?

Even in a simple variant like this, the JavaScript engine creates a local execution context:

var num = 2;
function pow(num) {
    var fixed = 89;
    return num * num;
}
pow(num);
Copy the code

Notice that I have added a variable named Fixed to the function POw. In this case, the local execution context will contain a box to hold the fixation.

I’m not very good at drawing small boxes in other small boxes! You have to use your imagination.

The local execution context will appear near the POW, contained in the green box within the global execution context. You can also imagine that for each nested function of a nested function, the engine creates more local execution contexts. These boxes can get to their destination quickly! Like a Russian nesting doll!

Now how about going back to the single-threaded story? What does that mean?

Single threaded JavaScript and other interesting stories

We say JavaScript is single-threaded because we have a Call Stack that handles our functions. That is, a function cannot leave the call stack if there are other functions waiting to be executed.

This is not a problem when dealing with synchronized code. For example, the sum between two numbers is synchronous and runs in microseconds. But what about network calls and other interactions with the outside world?

Fortunately, JavaScript engines are designed to be asynchronous by default. Even if they can execute one function at a time, there is a way for external entities to execute slower functions: browsers are an example. We’ll talk about that later.

In the meantime, you learned that when the browser loads some JavaScript code, the engine reads it line by line and performs the following steps:

  • Populating global memory (heap) with variable and function declarations
  • Push each function call to the call stack
  • Create a global execution context in which global functions are executed
  • Create lots of tiny local execution contexts (if there are internal variables or nested functions)

By now, you should have seen the synchronization mechanism on a per-javascript engine basis. In the sections that follow, you’ll see how asynchronous code works in JavaScript and why it works the way it does.

Asynchronous JavaScript, callback queues, and event loops

Global memory, execution context, and call stack explain how synchronous JavaScript code works in the browser. However, we are missing something. What happens when you have some asynchronous function running?

With asynchronous functions, each interaction with the outside world takes some time to complete. Calling the REST API or calling the timer is asynchronous, as they may take a few seconds to run. Using the elements we have in the engine so far, there is now a way to handle this function without blocking the call stack, and the same goes for browsers.

Remember that the call stack can execute one function at a time, and even a blocking function can freeze the browser directly. Fortunately, JavaScript engines are smart and can solve problems with the help of browsers.

When we run an asynchronous function, the browser gets the function and runs it for us. Consider the following timer:

setTimeout(callback, 10000);

function callback(){
    console.log('hello timer! ');
}
Copy the code

I’m sure you’ve seen setTimeout hundreds of times, but you probably didn’t know it wasn’t a built-in JavaScript function. That is, when JavaScript was born, there was no setTimeout built into the language.

In fact, setTimeout is part of what’s called the browser API, a collection of handy tools that browsers give us for free. This is good! What does this mean in practice? Since setTimeout is a browser API, this function is run directly by the browser (it is temporarily displayed in the call stack, but immediately removed).

Then 10 seconds later, the browser accepts the callback function we passed in and moves it to the callback queue. At this point we have two more boxes in our JavaScript engine. If you consider the following code:

var num = 2;
function pow(num) {
    return num * num;
}
pow(num);
setTimeout(callback, 10000);
function callback(){
    console.log('hello timer! ');
}
Copy the code

We can complete our illustration like this:

As you can see, **setTimeout runs in the browser context. After 10 seconds, the timer is triggered and the callback function is ready to run. But first it must pass through the callback queue. The ** callback queue is a queue data structure, which as its name implies is an ordered queue of functions.

Each asynchronous function must pass through the callback queue before being pushed onto the call stack. But who drives this feature? There is another component called Event Loop.

The Event Loop now has only one job: it should check if the Call Stack is empty. If there is some functionality in the callback queue, and if the call stack is free, then it is time to push the callback onto the call stack.

When complete, perform the function. Here’s a larger view of the JavaScript engine used to handle asynchronous and synchronous code:

Imagine that callback () is ready to execute. When pow () completes, the Call Stack is empty and the Event Loop pushes the callback (). That’s it! Even though I simplified a few things, if you understand the illustration above, then you can understand all JavaScript.

Remember: Browser apis, callback queues, and event loops are the backbone of asynchronous JavaScript.

If you like videos, I recommend watching Philip Roberts to watch the event loop anyway. This is one of the best explanations of Event Loop ever.

Hang in there, because asynchronous JavaScript isn’t done yet. In the next part, we’ll look at ES6 Promises in detail.

Call back to Hell and ES6 Promises

Callbacks are ubiquitous in JavaScript. They are used for synchronous and asynchronous code. Consider the map method, for example:

function mapper(element){
    return element * 2;
}
[1, 2, 3, 4, 5].map(mapper);
Copy the code

Mapper is a callback function passed in a map. The above code is synchronized. But consider an interval:

function runMeEvery(){
    console.log('Ran! ');
}
setInterval(runMeEvery, 5000);
Copy the code

The code is asynchronous, but as you can see, we pass the callback runMeEvery in the setInterval. Callbacks are common in JavaScript, so there has been a problem over the years: callback hell.

Callback hell in JavaScript refers to a programming “style” in which callbacks are nested within internally nested callbacks…… In other callbacks. Due to the asynchronous nature of JavaScript programmers have fallen into this trap for years.

To be honest, I have never encountered an extreme callback pyramid, perhaps because I value readable code and I always try to stick to it. If you end up with callback hell, you have too many features.

I won’t discuss callbackhell here, but if you’re curious there is a website, Callbackhell.com, that explores this problem in more detail and offers some solutions. “ES6 Promises” is one of the Promises we’re looking at. ES6 Promises is a complement to the JavaScript language designed to solve the dreaded callback hell. But anyway, what is Promise?

JavaScript promises are representations of future events. Commitments can end with success: in the jargon we’ve solved it. But if a Promise goes wrong, we say it’s in a rejection state. Promises also have a default state: each new Promise starts with a pending state. Can I make my own promises? Is. Let’s move on to the next section to see how.

Create and use JavaScript Promises

To create a new Promise, call the Promise constructor by passing it the callback data. Callbacks can take two arguments: resolve and reject. Let’s create a new Promise that will resolve after 5 seconds (you can try these examples in the browser console) :

const myPromise = new Promise(function(resolve){
    setTimeout(function(){
        resolve()
    }, 5000)});Copy the code

As you can see, resolve is a function created for our successful call to Promise. Reject produces a rejected Promise instead:

const myPromise = new Promise(function(resolve, reject){
    setTimeout(function(){
        reject()
    }, 5000)});Copy the code

Notice that in the first example, you can omit the reject because it is the second argument. But if you’re going to use reject, you can’t omit resolve. In other words, the following code will not work and will end up with a resolved Promise:

// resolve!
const myPromise = new Promise(function(reject){
    setTimeout(function(){
        reject()
    }, 5000)});Copy the code

Now, Promise doesn’t seem so useful, does it? These examples do not print anything to the user. Let’s add some data. Resolved and Rejected Promises can return data. Here’s an example:

const myPromise = new Promise(function(resolve) {
  resolve([{ name: "Chris" }]);
});
Copy the code

But we still don’t see any data. To extract data from a Promise, you link to a method called THEN. It needs a callback (ironically!). To receive actual data:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
myPromise.then(function(data) {
    console.log(data);
});
Copy the code

As consumers of JavaScript developers and other people’s code, you will interact primarily with Promises from the outside. Instead, the library author is more likely to wrap legacy code in the Promise constructor, as shown below:

const shinyNewUtil = new Promise(function(resolve, reject) {
  // do stuff and resolve
  // or reject
});
Copy the code

We can also create and resolve promises when needed by calling promise.resolve () :

Promise.resolve({ msg: 'Resolve! '})
.then(msg= > console.log(msg));
Copy the code

So, recall that JavaScript promises are bookmarks for future events. The event starts in the pending state and can succeed (resolved, performed) or fail (rejected). A Promise can return data and then extract it by attaching it to a Promise. In the next section, we’ll see how to handle errors from promises.

Mistakes in ES6 Promises

Error handling in JavaScript has always been simple, at least for synchronized code. Consider the following example:

function makeAnError() {
  throw Error("Sorry mate!");
}
try {
  makeAnError();
} catch (error) {
  console.log("Catching the error! " + error);
}
Copy the code

The output will be:

Catching the error! Error: Sorry mate!
Copy the code

An error is an expected catch block. Now let’s try using asynchronous functions:

function makeAnError() {
  throw Error("Sorry mate!");
}
try {
  setTimeout(makeAnError, 5000);
} catch (error) {
  console.log("Catching the error! " + error);
}
Copy the code

The above code is asynchronous due to setTimeout. What happens if we run it?

  throw Error("Sorry mate!");
  ^
Error: Sorry mate!
    at Timeout.makeAnError [as _onTimeout] (/home/valentino/Code/piccolo-javascript/async.js:2:9)
Copy the code

The output is different this time. The error did not pass the catch block. It is freely propagated through the stack.

That’s because try/catch only works with synchronized code. Error handling in Node.js explains this in more detail, if you’re curious.

Fortunately, promises have a way of handling asynchronous errors as if they were synchronous. If you recall the reject from the previous section, it makes a Promise to reject:

const myPromise = new Promise(function(resolve, reject) {
  reject('Errored, sorry! ');
});
Copy the code

In the example above, we can use the catch handler to handle the error, again taking the callback:

const myPromise = new Promise(function(resolve, reject) {
  reject('Errored, sorry! ');
});
myPromise.catch(err= > console.log(err));
Copy the code

We can also call promise.reject () to create and reject promises:

Promise.reject({msg: 'Rejected! '}).catch(err= > console.log(err));
Copy the code

To recap: When a Promise is filled, the then handler runs, while the catch handler runs Promises that were rejected. But that’s not the end of the story. We’ll see how async/await works well with try/catch later.

ES6 Promises combination: Promise. All, Promise. AllSettled, Promise

Promises don’t mean fighting alone. The Promise API provides a number of ways to group promises together. The most useful of these is promise.all, which accepts a Promises array and returns a Promise. When any Promise in the array is reject, promise. all returns reject.

As long as one of the promises in the array has a result, promise. race would be convergenceor reject. If one of the promises rejects, it still rejects.

Newer versions of V8 will also implement two new combinators: Promising.allSettled and Promising.any. Promise.any is still in the early stages of the proposal: as of this writing, there is still no support for it.

But the theory is promise. any can indicate whether any Promise satisfies resolve. Race differs from promise. race in that promise. any does not reject, even if one of the promises is reject.

Anyway, the most interesting of the two is Promise.AllSettled. It still needs a series of promises, but if one of them is reject, it doesn’t short-circuit. This is useful when you want to check that the Promise array is returned in full, whether it ends up being rejected or not. Think of it as the opposite of Promise. All.

ES6 Promises and MicroTask Queues

If you remember from the previous section, every asynchronous callback function in JavaScript ends up in the callback queue before being pushed onto the call stack. ** But the Callback functions passed in Promise have a different fate: they are handled by the Microtask Queue, not the Callback Queue.

You should note an interesting quirk: the microtask queue takes precedence over the callback queue. Callbacks from the microtask queue take precedence when the event loop checks to see if any new callbacks are ready to be pushed onto the call stack.

Jake Archibald covers these mechanisms in more detail in Tasks, microtasks, queues, and scheduling, which is worth a read.

JavaScript Engines: How do they work? Async /await: Promises to Async /await

JavaScript is evolving rapidly, and every year we improve the language. Promises seem to be the point of arrival, but ECMAScript 2017 (ES8) gives birth to a new syntax: async/await.

Async /await is just a stylistic improvement that we call syntactic sugar. Async /await does not change JavaScript in any way (remember that JavaScript must be backward-compatible with older browsers and should not break existing code).

It’s just a new way to write asynchronous code based on Promises. Let’s take an example. Previously we saved the Promise with the corresponding:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
myPromise.then((data) = > console.log(data))
Copy the code

Now with async/await we can look at synchronous asynchronous code from a reader’s point of view. We can wrap the Promise in a function labeled async and wait for the result:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
async function getData() {
  const data = await myPromise;
  console.log(data);
}
getData();
Copy the code

Does that make sense? Now, the interesting thing is that asynchronous functions will always return promises, and no one is stopping you from doing so:

async function getData() {
  const data = await myPromise;
  return data;
}
getData().then(data= > console.log(data));
Copy the code

What about mistakes? One benefit async/await provides is the opportunity to use a try/catch. Here’s how to handle errors in asynchronous functions and how to test them. Let’s look at promises again. We use a catch handler to handle errors:

const myPromise = new Promise(function(resolve, reject) {
  reject('Errored, sorry! ');
});
myPromise.catch(err= > console.log(err));
Copy the code

Using asynchronous functions, we can refactor the following code:

async function getData() {
  try {
    const data = await myPromise;
    console.log(data);
    // or return the data with return data
  } catch (error) {
    console.log(error);
  }
}
getData();
Copy the code

Not everyone accepts the style. Try /catch can make your code noisy. There is another quirk to be pointed out, though, with try/catch. Consider the following code that raises an error in a try block:

async function getData() {
  try {
    if (true) {
      throw Error("Catch me if you can"); }}catch (err) {
    console.log(err.message);
  }
}
getData()
  .then((a)= > console.log("I will run no matter what!"))
  .catch((a)= > console.log("Catching err"));
Copy the code

Which of the two strings is printed to the console? Remember that try/catch is a synchronous construct, but our asynchronous function produces a Promise. They travel on two different tracks, like two trains.

But they will never meet! That is, an error thrown by a throw never triggers a catch handler for getData (). Running the code above results in “Catch me if you can”, followed by “I will run no matter what!” .

In the real world, we don’t want the throw to trigger the handler at the time. One possible solution is to return promise.reject () from the function:

async function getData() {
  try {
    if (true) {
      return Promise.reject("Catch me if you can"); }}catch (err) {
    console.log(err.message); }}Copy the code

Error handling is now as expected:

getData()
  .then((a)= > console.log("I will NOT run no matter what!"))
  .catch((a)= > console.log("Catching err"));
"Catching err" // output
Copy the code

Beyond that, async/await seems to be the best way to build asynchronous code in JavaScript. We have better control over error handling and the code looks cleaner.

However, I do not recommend refactoring all JavaScript code into async/await. These are options that must be discussed with the team. But if you work alone, whether you use simple Promises or async/await it is a matter of personal preference.

conclusion

JavaScript is a scripting language for the Web with features that are first compiled and then interpreted by the engine. Among the most popular JavaScript engines are V8 for Google Chrome and Node.js, SpiderMonkey for the web browser Firefox, and JavaScriptCore for Safari.

JavaScript engines have a lot of exciting parts: call stacks, global memory, event loops, callback queues. All of these parts work together in perfect tune to handle both synchronous and asynchronous code in JavaScript.

The JavaScript engine is single-threaded, which means there is a Call Stack for running functions. This restriction is fundamental to the asynchronous nature of JavaScript: all actions that take time must be taken care of by external entities (such as browsers) or callback functions.

To simplify asynchronous code flow, ECMAScript 2015 brings Promise. Promise is an asynchronous object that represents the failure or success of any asynchronous operation. But the improvements don’t stop there. In 2017, async/await was born: it was a stylistic complement to Promise, making it possible to write asynchronous code as if it were synchronous.

Thanks for reading and stay tuned to this blog!