The JavaScript Event Loop
How JavaScript can be single threaded and asynchronous.
I recently had a phone interview and was asked some questions that I could have answered better. The purpose of this series of blog posts is to help me remember these concepts, so next time I will have a better answer. If others find these posts useful, all the better.
JavaScript is single threaded — there is only one execution path through the code. The JavaScript Call Stack handles this execution. The Call Stack is an array-like object that keeps track of where you are in the code. It works on a Last On First Off (LIFO) basis. I think the best way to illustrate this is to use an example:
const first = () => {
console.log(‘First function’);
return second();
}const second = () => {
console.log(‘Second function’);
return third();
}const third = () => {
console.log(‘Third function’);
return ‘Finished’
}console.log(first());
Here is how this code will interact with the Call Stack:
- The first thing added to the call stack is the
console.log(first())
statement at the end of the block of code. Before this are three function declarations. These are stored in memory, but do not interact with the Call Stack until they are called. - The
first()
function is added to the Call Stack. - Next
console.log(‘first function’)
is added to the Call Stack. It is executed and removed from the call stack. return second()
is added to the Call Stack which calls thesecond()
function.- The
second()
function is added to the Call Stack. - Next
console.log(‘second function’)
is added to the Call Stack. It is executed and removed from the call stack. return third()
is added to the Call Stack which calls thethird()
function.- The
third()
function is added to the Call Stack. - Next
console.log(‘third function’)
is added to the Call Stack. It is executed and removed from the call stack. - Then
return ‘Finished’
is added to the Call Stack, executed and removed. - The
third()
function is now finished and removed from the Call Stack. return third()
from thesecond()
function is executed and removed from the Call Stack.- The
second()
function is now finished and removed from the Call Stack. return second()
from thefirst()
function is now executed and removed from the Call Stack.- The
first()
function is now finished and removed from the Call Stack. - Finally, the
console.log(first())
, which was added in step 1, can be executed and removed from the Call Stack. Now all the code has been executed.
After running the code, the console looks like this:
“1st function”
“2nd function”
“3rd function”
“Finished”
Asynchronous Code
This works great for code that can be executed quickly. For code that takes longer, such as making an AJAX request, how can we prevent the website from becoming unresponsive until the code is finished. This is where the event loop comes in. Any asynchronous code (code that cannot be executed right away), such as an API call, an event listener, or a setTimeout, is removed from the Call Stack and sent to the Event Table. It will wait there until the event it needs to complete execution occurs. An AJAX request will wait on the Event Table until the data it is waiting for returns. A setTimeout will wait on the Event Table until the set number of milliseconds have passed. After this, the asynchronous code is moved to the Event Que, where is waits until the Call Stack is empty. The Event Que operates on a First On First Off (FIFO) basis. Once the Call Stack is empty, the asynchronous code that has been in the Event Que the longest is moved to the Call Stack and executed. This continues until the Event Que and the Call Stack are empty.
To illustrate this, let’s take the example from above, alter it a little to add an asynchronous operation, and trace its execution through the Call Stack and Event Loop. Changes are bold.
const first = () => {
console.log(‘First function’);
return second();
}const second = () => {
setTimeout(() => console.log(‘Second function’));
return third();
}const third = () => {
console.log(‘Third function’);
return ‘Finished’
}console.log(first());
Here is how this code will interact with the Call Stack and Event Loop:
- The first thing added to the call stack is the
console.log(first())
statement at the end of the block of code. - The
first()
function is added to the Call Stack. - Next
console.log(‘first function’)
is added to the Call Stack. It is executed and removed from the call stack. return second()
is added to the Call Stack which calls thesecond()
function.- The
second()
function is added to the Call Stack. - Next
setTimeout(() => console.log(‘Second function’))
is added to the Call Stack. SincesetTimeout
isasynchronous, it is removed from the Call Stack and placed on the Event Table until the number of milliseconds specified in thesetTimeout
function has passed. - Since we did not specify any delay in the
setTimeout
function (the default value is zero),setTimeout(() => console.log(‘Second function’))
is moved immediately to the Event Que where it waits until the Call Stack is empty. return third()
is added to the Call Stack which calls thethird()
function.- The
third()
function is added to the Call Stack. - Next
console.log(‘third function’)
is added to the Call Stack. It is executed and removed from the Call Stack. - Then
return ‘Finished’
is added to the Call Stack, executed and removed. - The
third()
function is now finished and removed from the Call Stack. return third()
from thesecond()
function is executed and removed from the Call Stack.- The
second()
function is now finished and removed from the Call Stack. return second()
from thefirst()
function is now executed and removed from the Call Stack.- The
first()
function is now finished and removed from the Call Stack. console.log(first())
, which was added in step 1, can be executed and removed from the Call Stack.- The Call Stack is now empty, so
setTimeout(() => console.log(‘Second function’))
, which has been waiting in the Event Que, can now be moved to the Call Stack. - Finally,
setTimeout(() => console.log(‘Second function’))
is now executed and removed from the Call Stack. Now all the code has been executed.
After running the code, the console looks like this:
"First function"
"Third function"
"Finished"
"Second function"
Conclusion
Hopefully, this illustrates how JavaScript handles asynchronous code. This can get complicated quickly, so we had to keep our examples simple. But even code with more asynchronous calls operates on the same principles. For further information see Concurrency model and Event Loop on the Mozilla Developers Network.
Other Stories in this Series: