Most experienced and new JavaScript developers come across the terms - Single-Threaded, Multi-Threaded, Blocking, Non-Blocking, Synchronous, Asynchronous, Call Stack, and more. However, they do not know the meaning and importance of those concepts in JavaScript. Thus, I attempt to explain those terms in this article and additionally state why Brendan Eich, the creator of JavaScript, used this approach.
Let us start by understanding what single-threaded and synchronous means and how it works.
What do Single-Threaded and Synchronous mean?
Let's begin with "JavaScript is a single-threaded synchronous language."
The single-threading term refers to our program getting executed one line at a time individually because JS only contains one Call Stack and one Memory Heap. That means the upcoming statements in the program cannot get executed until the current one completes its execution. It slows down the performance time because each concise function takes lots of time to complete, and we cannot operate functions simultaneously. It is like a human trying to work simultaneously or multi-tasking on complex tasks with limited attentional space, but they fail. Instead, they finish one assignment at a time.
Synchronous (sync) refers to the process that executes code lines sequentially, one after the other. When we combine the behaviours of single-threaded and synchronous, we get a programming language that implements one line of code at a given time sequentially without skipping any statement in between. Nonetheless, such behaviour also gives rise to blocking.
To better understand the synchronous behaviour of JavaScript, we can use an example from Sharjeel Siddique's article on Synchronous and Asynchronous JS. He says, for example, imagine yourself calling someone whilst waiting for them to answer your call. You're not doing any other task until the receiver picks up the call. You only start talking once the receiver accepts the request. Well, Synchronous JavaScript works in the same way. It waits for the current task to get executed to begin a new one.
In another example, imagine yourself as a cook. You must follow the given recipe sequentially. You cannot skip any instruction or ingredient unless you want to become as good as Mr Bean at cooking food. And the synchronous paradigm ensures that each instruction in the recipe gets fulfilled sequentially. Take a look at the code snippet below.
console.log(`Afan`);
console.log(`likes`);
console.log(`JavaScript`);
*
Output:
Afan
likes
JavaScript
*/
Since JS executes each statement in a sequence, it logs each string to the console in the same order of lines as we wrote them.
I hope you understand the base concept of Single-Threading and Synchronous. Now, let us see how JavaScript embeds these behaviours into its browser-based engine, perhaps which makes JavaScript a single-threaded language.
Introduction to the Call Stack
Each statement, function, and code line goes through the Call Stack to get executed in JavaScript. And this happens with the help of Global and Standard Execution Contexts pushed to the Call Stack sequentially. However, Execution Contexts are outside of this article's scope for now. I will write a separate paper to explain them.
Nonetheless, the JS Call Stack is a combination of single-threading and synchronous behaviours. It is inside the runtime environment of the browser's JS engine. For instance, Google Chrome has its V8 engine to execute JavaScript programs.
The Call Stack lives inside the runtime environment, with the Memory Heap as a crucial component. It only executes one statement at a time and proceeds sequentially throughout our program. We cannot skip any line or simultaneously run another function while the current statement continues to operate in the Call Stack.
It accepts code lines through execution contexts from your programs and throws them out of the stack once the task gets completed. It keeps stacking each task on top of the previous one, like CDs in a circular tray, books on your shelf, or moulded chairs after a wedding event. The last function that gets added to the stack is the one that gets executed first. It tracks the statement that the interpreter of JavaScript is currently running while stacking the remaining execution contexts.
When dealing with functions, the function call enters the call stack first, followed by the code lines inside the function body. The statements inside the body get stacked on top of each other in the Call Stack in the same sequence as written in the program. Once all the code lines complete their execution sequentially, they get thrown out of the call stack. After their removal, even the function call gets removed. Take a look at the example below.
If we have a function call inside another function, the JS engine will push the nested function to the Call Stack first, execute and obtain a value, and then continue with the parent function. While it goes through the nested loop, the execution of the parent loop pauses since JS works synchronously.
Some might have noticed that this approach becomes immensely inefficient while writing complex algorithms and heavy computations in today's world. And you're correct to imply that ideology. And that is why we need asynchronous parallel execution for our modern JavaScript programs. However, let us hold on to that thought for now and continue with the article. We will get back to this.
Since JS merely use the Call Stack to execute our code lines, which heavily uses the concept of Single-Threading and Synchronous processes, I think it is crucial to understand why Brendan chose that approach.
Why is JS single-threaded?
Initially, the design of JavaScript got created to assist a single user on a browser-based window with not-so-complex websites. For context, JS got released in 1995 through the Netscape Corporation, formerly named Mosaic Corp. The demands of consumers and developers weren't peaking years ago, irrespective of the dot-com boom. At that time, single-threaded behaviour was common with simple websites. However, as time and the internet progressed, JavaScript became a popular language for multiple use cases and millions of developers around the globe.
We started making complex algorithms and programs requiring heavy computational designs to accommodate modern demands. But JavaScript failed at that miserably. It couldn't execute complex programs due to the core synchronous single-threaded design. JavaScript took significant time to run composite programs because each statement got executed one at a time.
However, it wasn't feasible to change the design of the entire language. We couldn't change the fundamentals of the language. Therefore, ECMA introduced mechanisms like Callbacks, Promises (ES6), Async/await (ES8), etc. Additionally, developers in recent years started using Web Workers to allow asynchronous parallel execution in separate threads in JavaScript.
As we couldn't change the fundamentals of the language, we got stuck with the synchronous single-threaded behaviour. However, we came up with a different approach to execute programs asynchronously. And now, it's time to understand that unique approach.
What does Asynchronous mean? [The Unique Approach]
Well, Asynchronous is the contrary of Synchronous. It accepts statements and executes many of them simultaneously while continuing the execution of the rest of the program sequentially.
Let us go back to the call analogy. While waiting for the receiver to pick up the call, we perform other tasks like laundry, dishwashing, clearing your hard drive, and more simultaneously. We are not waiting for one chore to get completed to start another one.
For example, take a look at the code below.
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000);
console.log(3);
/*
Output:
1
3
2
*/
Try to run this code in your browser. The numbers '1' and '3' get printed to the console immediately, while '2' takes a second (1000ms) after printing '1' to log itself. Until the timer runs out, the JS interpreter executes the rest of the program sequentially. It doesn't wait for 1000ms to continue the program. And this happens because the setTimeout() method acts as an asynchronous method. After all, it contains a callback function. If you remember, a Callback function is a way to implement an asynchronous behaviour.
JavaScript calls the setTimeout() function in the Call Stack and then suddenly pops it out to shift it to the Web APIs component. The Web APIs component turns the timer on. Once the 1000ms timer runs out, the Callback function gets thrown to the Callback Queue in the browser, and then the event loop sends it back to the Call Stack to execute it when the Call Stack gets empty.
The job of the event loop is to wait for the entire Call Stack to become empty to push the Callback functions from the Callback Queue to the Call Stack. Look at the representation of the above code in a graphical approach below from Philip Robert's tool cited in his JSConf EU Talk.
Even though we set a timer of executing '2' after a second of logging '1' to the console, the number '3' immediately gets printed. It doesn't wait for the timer to run out. And this is how asynchronous works. It doesn't wait for the previous task to get completed. For reference purposes, we named the callback function timeout().
When using real-world APIs, the Callback functions of the APIs shift to the Web APIs component until the API request gets resolved. Usually, these requests take a lot of time, and we cannot afford to wait till they get completed. Therefore, we keep them in the Web APIs component and execute the rest of the program simultaneously without interruptions or slowness. Once the request gets resolved, the function goes to the Callback queue and then to the Call Stack once it gets empty.
Even when you set the setTimeout() timer to 0ms, it passes through the Web APIs component. We use this to execute something after the entire Call Stack gets empty. The Callback function gets executed immediately inside the Web APIs component as we set it to 0ms and gets thrown to the Callback Queue. Once the Call Stack clears, the event loop passes this callback function (timeout()) to the Call Stack for it to get executed.
These mechanisms make JavaScript non-blocking. Before I conclude, allow me to explain this last term.
What the heck is non-blocking?
When our program contains multiple statements with API requests or functions that take lots of time to complete, we need a non-blocking system to avoid blocking the execution of the remaining code lines. For example, if we send an API request in the middle of the program, the rest of the execution gets blocked until the API request gets resolved while assuming we are in a synchronous state. And that is how blocking works.
When we enter the asynchronous state, we offload the API requests to the Web APIs component. While the rest of the statements execute, the API request on the other side gets resolved simultaneously, thereby achieving a non-blocking state.
By definition, non-blocking means that an algorithm doesn't fail or stop executing the remaining code lines when there is a function which takes lots of time to run or an error which could terminate the execution. In another depiction, a non-blocking system continues to execute the rest of the program even if it encounters an error.
Summary
JavaScript is the core choice for many developers nowadays. We can't change the fundamentals of the language anymore, nor the quirky name. But we can understand the negatives of it and find alternatives to solve a problem using JS. Our job as a developer is to develop solutions for as many problems as possible, and we cannot wait for a dominant modification to come and fix the language. Not to mention, the probability of such change remains extremely low. Therefore, your best bet is to comprehend these terms thoroughly and use them in your daily problem-solving software development life.
By the way, I am writing a book called CrackJS, which is based on and covers the fundamentals of JavaScript. If you want to learn more about JS, follow me on this journey of writing this book with these articles trying to help developers worldwide.
If you want to contribute to this article, drop a comment with your opinion, and if I should change anything in this article. I am also available via E-mail at hello@afankhan.com.
Top comments (0)