The JavaScript Event Loop

How JavaScript can be single threaded and asynchronous.

Scott Price
5 min readOct 2, 2018

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:

  1. 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.
  2. The first() function is added to the Call Stack.
  3. Next console.log(‘first function’) is added to the Call Stack. It is executed and removed from the call stack.
  4. return second() is added to the Call Stack which calls the second() function.
  5. The second() function is added to the Call Stack.
  6. Next console.log(‘second function’) is added to the Call Stack. It is executed and removed from the call stack.
  7. return third() is added to the Call Stack which calls the third() function.
  8. The third() function is added to the Call Stack.
  9. Next console.log(‘third function’) is added to the Call Stack. It is executed and removed from the call stack.
  10. Then return ‘Finished’ is added to the Call Stack, executed and removed.
  11. The third() function is now finished and removed from the Call Stack.
  12. return third() from the second() function is executed and removed from the Call Stack.
  13. The second() function is now finished and removed from the Call Stack.
  14. return second() from the first() function is now executed and removed from the Call Stack.
  15. The first() function is now finished and removed from the Call Stack.
  16. 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:

  1. The first thing added to the call stack is the console.log(first()) statement at the end of the block of code.
  2. The first() function is added to the Call Stack.
  3. Next console.log(‘first function’) is added to the Call Stack. It is executed and removed from the call stack.
  4. return second() is added to the Call Stack which calls the second() function.
  5. The second() function is added to the Call Stack.
  6. Next setTimeout(() => console.log(‘Second function’)) is added to the Call Stack. Since setTimeout isasynchronous, it is removed from the Call Stack and placed on the Event Table until the number of milliseconds specified in the setTimeout function has passed.
  7. 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.
  8. return third() is added to the Call Stack which calls the third() function.
  9. The third() function is added to the Call Stack.
  10. Next console.log(‘third function’) is added to the Call Stack. It is executed and removed from the Call Stack.
  11. Then return ‘Finished’ is added to the Call Stack, executed and removed.
  12. The third() function is now finished and removed from the Call Stack.
  13. return third() from the second() function is executed and removed from the Call Stack.
  14. The second() function is now finished and removed from the Call Stack.
  15. return second() from the first() function is now executed and removed from the Call Stack.
  16. The first() function is now finished and removed from the Call Stack.
  17. console.log(first()), which was added in step 1, can be executed and removed from the Call Stack.
  18. 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.
  19. 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:

--

--

Scott Price
Scott Price

Written by Scott Price

Front End Developer with Information Security Experience. See my resume and portfolio at www.scottaprice.com