JavaScript Essentials: Often Overlooked Elements of One of The Most Popular Web Development Languages
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:
- The runtime pushes a number of instructions from your code to the execution stack.
- If any of those instructions are asynchronous, they are moved to be executed outside of the main thread, at the Operating System level.
- The asynchronous task is removed from the stack while its actual execution is running in parallel.
- The main thread continues with the execution of the remaining tasks in the stack.
- 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. - The function with the result is queued to wait for its turn to execute in the main thread.
- 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:
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!