JavaScript Essentials: Often Overlooked Elements of One of The Most Popular Web Development Languages

Omar Verduzco
SSENSE-TECH
Published in
7 min readNov 12, 2021

Part 2 of the 3-part series on JavaScript essentials by SSENSE-TECH. Read Part I focused on the usage of the keyword this.

At SSENSE we leverage Nodejs apps because of the runtime’s non-blocking nature and fast performance. This allows us to serve thousands of requests per second flawlessly, especially during high-traffic events, giving our customers a seamless experience. In this second article of the JavaScript Essentials series, I will cover how the execution of asynchronous tasks works. You’ve probably heard of callbacks, the callback hell, the concept of Promise, async-await, and the event loop; here I will connect all the concepts and explain how they work.

EVENT LOOP

Let’s remember that JavaScript was designed to be a language to run in web browsers, allowing for increased functionality on websites. Its main purpose is to take user experience to the next level. By design it is single-threaded, which means that any operation that takes a long time to execute (e.g. I/O operations like HTTP calls) could block the interaction with the page. Of course this would be unacceptable from a UX perspective, so a mechanism had to be introduced to allow for asynchronous execution without blocking the main thread.

The event loop is the mechanism that solves this problem. Below are the steps that it implies:

  1. The runtime pushes a number of instructions from your code to the execution stack.
  2. If any of those instructions are asynchronous, they are moved to be executed outside of the main thread, at the Operating System level.
  3. The asynchronous task is removed from the stack while its actual execution is running in parallel.
  4. The main thread continues with the execution of the remaining tasks in the stack.
  5. Whenever the asynchronous task is complete, the result is passed to the function that you declared to be the handler of said result (a.k.a. the callback function). This still happens outside of the main thread.
  6. The function with the result is queued to wait for its turn to execute in the main thread.
  7. Once the main thread is out of tasks, all the queued callbacks, which already have the result of the asynchronous tasks, are pushed back into the stack to be executed.

This design allows for continuous interaction with the website, but also brings challenges for developers as we are used to thinking sequentially when programming.

Here is a graphical representation of the steps mentioned above:

Event Loop

Now that we understand how the mechanism works, let’s understand the following concepts:

CALLBACKS

As explained in the first article of this series, functions are objects that you can pass as parameters to other function calls. Generally, callbacks represent the function that will take the result of the asynchronous task and do something with it once allowed to enter the stack. Here’s an example of how an async HTTP call would look like:

I would like to highlight that callbacks are not only used for asynchronous execution but are also commonly found in built-in functionalities, for example, iteration methods in arrays.

While understanding the concept of a callback can be useful to successfully achieve functionality, using them often brings known problems, and a famous problem in the language was named the “callback Hell”. Imagine having to execute sequential tasks and all of them require I/O operations, your code could end up looking like this:

This code is not easy to read, maintain, or extend, but that is not the only problem with callbacks. To understand an arguably bigger problem, let’s imagine that we’re developing an e-commerce application that uses a third party library to verify the authenticity of a credit card. Consider this sample code:

If you read the code, you could understand that it first verifies the card and then captures the payment, which seems logical, but what happens if the library you’re using has a bug or poor error handling (e.g. silent errors), and somehow never executes your callback to charge the customer? You’d be giving stuff away for free. The problem that I’m trying to highlight here is that you could end up with issues that are very difficult to debug if you blindly trust that all your third-party libraries are bug-free.

PROMISES

Promises are the way to give the control back to the developer. They still use callbacks, but instead of passing them around between libraries, they are passed to the runtime to execute whenever the asynchronous task is complete and has a result. That result is passed to the callback function and the same event loop flow described above takes place.

This guarantees that instead of relying on someone else’s code to properly execute and handle your callbacks, you get the result of the asynchronous operation and tell the runtime what to do with it.

A Promise looks like this:

Promises can either be fulfilled (a.k.a. resolved) or rejected. When instantiating a new Promise, a callback is passed to the constructor. This callback should include the asynchronous task, and it also receives two functions as arguments; the first one resolves the Promise with a result, while the second one rejects the Promise with a reason. Look at this example:

As mentioned, our callback will now be executed by the runtime rather than a third party library. In order to do this, Promises include the then method, which takes our callback as an argument. This callback receives one parameter which is the result of the Promise if it was resolved. Similarly, Promises include the catch method, which takes another parameter that represents the reason why it was rejected. This way the developer takes back control of the flow even when using third-party code.

There are several properties and features in Promises that allow you to write more readable and maintainable code, but diving into them would be out of this article’s scope. If you wish to dig deeper, please refer to the Additional Resources and Related Links section.

ASYNC-AWAIT

Even with the improvement that Promises provides, code readability was still challenging because it was still not easy to represent as sequential and synchronous code like in most other languages. Because of this, async-await was implemented and it allowed us to write code in a synchronous-looking fashion.

It is important to highlight that, while async-await feels like synchronous code, in reality it is only a wrapper for Promises. The syntax is as follows:

An async function will always return a Promise. Even if you state a return value, it will be wrapped into a Promise that is resolved with that value. If something goes wrong within the function, it will return a Promise that is rejected with the error that occured.

In order to use await inside a function, it has to be declared to be async. This is a very interesting feature, because it changes what we thought we knew about asynchronous execution. In this case, await will wait for the Promise in front of it to fully execute and pause the execution only inside the async function until said Promise is either resolved or rejected.

USING AN async FUNCTION

When you call an async function, since its return value will always be a Promise, the same rules of the event loop apply. That means that, unless you call your async function with await, it will be executed at the OS level and the execution in the main thread will move on to the next line. If you call it with await, it will pause the execution until the return value is available and then move on.

Even if async-await is a wrapper for Promises, the .then() and .catch() syntax is not available in this case. So, how does error handling look in this case? It is simply a try-catch statement.

FINAL THOUGHTS

Asynchronous execution in JavaScript can be tricky, and this was one of the main reasons why the language had been criticized. However, Promises and async-await provide interesting features that — when understood properly — allow us to write clean, maintainable, and extendable code. That said, it is still important to dedicate time to understanding how all these mechanisms work and why they were implemented, because if not used properly, we can still end up with bugs that are very difficult to debug.

Stay tuned for Part III of the JavaScript Essentials series, which will dive into closures and scope.

ADDITIONAL RESOURCES AND RELATED LINKS

Editorial reviews by Liela Touré, and Pablo Martinez. Want to work with us? Click here to see all open positions at SSENSE!

--

--