π» Event Loop Intro
Introduction π§
This article is a light-hearted spin on Tania Rascia's event loop article for halloween. Make sure to check out her full article: Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript.
The Main Thread
Before we jump into the basic concepts of the event loop, the first thing you should understand is:
"JavaScript is a single-threaded programming language with a synchronous execution model that processes one operation after another, it can only process one statement at a time."
You'll often hear this referenced throughout the web as the 'Main Thread'. In JavaScript, we can't spin up additional threads that run in parallel to help out with the computation cost of your code across multiple CPU cores. With a single threaded model, you need to be careful not to block the main thread.
What do I mean by block the main thread? Well, UI interactions are handled on the same thread. If a synchronous task takes 10 seconds on the main thread, the user won't be able to interact with your site during this time, which isn't a very pleasant experience.
For me, the nice thing about a single threaded model is not having to worry about concurrency across multiple threads. In saying this, it would be pretty neat to spin up another thread for long client side computations without impacting the users interactions on your site. So how do we get around this problem?
I'm not going to cover the topic completely in this article, but Web Workers as a Web API makes it possible to:
"Run a script operation in a background thread separate from the main execution thread of a web application. The advantage of this is that laborious processing can be performed in a separate thread, allowing the main (usually the UI) thread to run without being blocked/slowed down.
Handling Asynchronous Tasks
When we click a button on the UI that performs an action like requesting data from an API:
"it can take an indeterminate amount of time, depending on the size of data being requested, the speed of the network connection, and other factors. If API calls were performed in a synchronous manner, the browser would not be able to handle any user input, like scrolling or clicking a button, until that operation completes. This is known as blocking. In order to prevent blocking behaviour, the browser environment has many Web APIs that JavaScript can access that are asynchronous, meaning they can run in parallel with other operations instead of sequentially."
Now that we have an insight into the execution model, here's the spooky synchronous execution below, where each operation runs synchronously one after another, processing one statement at a time.
Synchronous Example
1const changeEmojiDisplay = (type = 'show') => {2 console.log('2');3 const elements = document.getElementsByClassName('spooky-emoji');4 for (let element of elements) {5 console.log('(3) change element className');6 element.className = `spooky-emoji spooky-emoji--${type}`;7 }8 };910 const changeTextDisplay = (type = 'show') => {11 console.log('5');12 const elements = document.getElementsByClassName('spooky-text');13 for (let element of elements) {14 console.log('(6) change element className');15 element.className = `spooky-text spooky-text--${type}`;16 }17 };1819 const start = () => {20 console.log('1');21 changeEmojiDisplay();22 console.log('4');23 changeTextDisplay();24 console.log('7');25 };2627 const reset = () => {28 changeEmojiDisplay('hide');29 changeTextDisplay('hide');30 }
Call Stack πΈοΈ
- start() function is triggered by a button click, added to the stack and run. (#19)
- changeEmojiDisplay() function is added to the stack and run. (#21)
- Grabs the collection of elements. (#3)
- Iterates the HTMLCollection of elements & adds a new class to each one. (#3 - #6)
- changeEmojiDisplay() function is removed from the stack.
- The same happens for the changeTextDisplay() function, which is added to the stack next (#23) , runs and performs similar operations (#10 - #17) before it is also removed from the stack.
- start() function is removed from the stack and execution is finished. (#25)
Console Output π·οΈ
- 1
- 2
- (3) change element className
- 4
- 5
- (6) change element className
- 7
Asynchronous Example
Sure... that was easy and super obvious! If only all execution was nice and simple and synchronous right?! Now let's add some asynchronous code into the mix. I've created an almost identical example below, but added setTimeout (a built-in Web API) into each loop. Here's the spooky asynchronous execution below.
1const changeEmojiDisplayAsync = (type = 'show') => {2 console.log('2');3 const elements = document.getElementsByClassName('spooky-emoji-async');4 for (let element of elements) {5 setTimeout(() => {6 console.log('(3) change element className');7 element.className = `spooky-emoji-async spooky-emoji-async--${type}`;8 }, 2000);9 }10 };1112 const changeTextDisplayAsync = (type = 'show') => {13 console.log('5');14 const elements = document.getElementsByClassName('spooky-text-async');15 for (let element of elements) {16 setTimeout(() => {17 console.log('(6) change element className');18 element.className = `spooky-text-async spooky-text-async--${type}`;19 }, 0);20 }21 };2223 const startAsync = () => {24 console.log('1');25 changeEmojiDisplayAsync();26 console.log('4');27 changeTextDisplayAsync();28 console.log('7');29 };3031 const resetAsync = () => {32 changeEmojiDisplayAsync('hide');33 changeTextDisplayAsync('hide');34 };
Call Stack πΈοΈ
- startAsync() function is triggered by a button click, added to the stack and run.(#23)
- changeEmojiDisplayAsync() function is added to the stack and run (#25), which:
- Grabs the collection of elements. (#3)
- Iterates the HTMLCollection of elements. (#4 - #9)
- Add four setTimeout() functions to the stack, run each setTimeout() Web API which starts a timer and adds each anonymous function to the queue, remove each setTimeout() from the stack. (#5 - #8)
- changeEmojiDisplayAsync() function is removed from the stack. (#26)
- changeTextDisplayAsync() function is added to the stack and run (#27) which:
- Grabs the collection of elements. (#14)
- Iterates the HTMLCollection of elements. (#15 - #20)
- Add four setTimeout() functions to the stack, run each setTimeout() Web API which starts a timer and adds each anonymous function to the queue, remove each setTimeout() from the stack. (#16 - #19)
- The event loop checks the queue for anything pending and finds the anonymous functions (8 in total) added by each setTimeout(). The event loop adds each function in the queue to the stack, runs it, then removes it from the stack.
Console Output π·οΈ
- 1
- 2
- 4
- 5
- 7
- (6) change element className
- (3) change element className
Understanding Execution
One thing you'll notice when looking at the code above is the setTimeout added with a value of 0. Developers new in the JavaScript space might assume this executes the anonymous function inside it immediately at 0 seconds, but we know thats not the case! If you're checking this article out on desktop, make sure to pop open the developer tools and navigate to the console to see the log order happen as you interact with the start and refresh buttons. Did you notice how '7' logs before '6'?
"Whether you set the timeout to zero seconds or five minutes will make no difference: the console.log called by asynchronous code will execute after the synchronous top-level functions. This happens because the JavaScript host environment, in this case the browser, uses a concept called the event loop to handle concurrency, or parallel events. Since JavaScript can only execute one statement at a time, it needs the event loop to be informed of when to execute which specific statement. The event loop handles this with the concepts of a stack and a queue."
With this in mind, alongside seeing a synchronous and asynchronous example, we're ready to take a look at the importance roles the stack and queue play in the event loop to ensure the main thread runs smoothly.
The event loop checks: is the call stack empty? If yes, push the next task from the queue onto the stack.
Stack β οΈ
The call stack runs inside the host environments JavaScript engine, in this case we are talking about the browser. This single-thread interpreter also has a memory heap that sits alongside the call stack. The heap acts as a place to store and write information inside our applications (allocate, use and release memory), whereas the stack keeps track of our current execution. Tania Rascia's article sums the callstack up nicely:
"The stack, or call stack, holds the state of what function is currently running. If you're unfamiliar with the concept of a stack, you can imagine it as an array with "Last in, first out" (LIFO) properties, meaning you can only add or remove items from the end of the stack. JavaScript will run the current frame (or function call in a specific environment) in the stack, then remove it and move on to the next one."
Queue π
In the asynchronous spooky examples we're using above, we have a callback function, an event loop, a call stack and a task queue at play. Each anonymous callback function runs its execution on the call stack after the call back function has been pushed to the stack by the event loop. So how does this all happen?! Well, I don't think I could explain it any better than Tania Rascia's article:
"The queue, also referred to as message queue or task queue, is a waiting area for functions. Whenever the call stack is empty, the event loop will check the queue for any waiting messages, starting from the oldest message. Once it finds one, it will add it to the stack, which will execute the function in the message."
In our spooky setTimeout example, each of our anonymous functions run two seconds after the rest. As I've mentioned earlier (setTimeout 0) It's important to remember that the function will NOT execute EXACTLY 2 seconds after we reach the setTimeout function in the queue or whatever the time set is. So why don't we just add the anonymous functions to the call stack when the timer finishes? Well that would defeat the purpose of having a queue at play in the event loop, and introduce some problems!
"This queue system exists because if the timer were to add the anonymous function directly to the stack when the timer finishes, it would interrupt whatever function is currently running, which could have unintended and unpredictable effects."
There is also another queue involved in the event loop, often referred to as the "microtask queue" that handles promises, but it adds some additional complexity that feels out of scope for this article, but its worth knowing it exists.
Final Thoughts
That's it for this article, make sure to check out the video below to see a visual representation of the event loop and set timeouts in action.
Was this useful?
Thanks for reading! Time for one more?
// Sketchy's journal β the whole DB// boils down to four tables and a// single sync expo-sqlite handle.const db = SQLite.openDatabaseSync('journal.db')
πΉ Sketchy - Skateboard Companion App
03 May 2026 β’ π 30 min readBehind the scenes of Sketchy, a skate companion app built on Expo SDK 55, React Native 0.83, expo-sqlite, OpenStreetMap and EAS, with the New Architecture and React Compiler turned on.
const intervalRef = useRef(null)const resetGame = useCallback(() => {clearInterval(intervalRef.current)}, [])
πΉοΈ Building 'Simple' React Games
21 Apr 2026 β’ π 19 min readWhat I learned refactoring two browser games: grouping state, useReducer, stale closures, timer cleanup, render-time randomisation bugs, and replacing three intervals with one rAF loop.