Asynchronous JavaScript and the Event Loop: How Do Promises Work?

JavaScript is a Single Threaded language. This means it can only process one task at a time. However, the websites we use don't behave that way. We can scroll down a page while data is being fetched, or click a button while an image is loading.
If it can only do one thing at a time, how does it look like everything is happening simultaneously?
The secret lies in the Event Loop, which works hard behind the scenes of the JavaScript engine. Today, we will dig into the principles of asynchronous processing in JavaScript—a concept every React developer must understand.
The Dilemma of the Single Thread
Let's assume Chris is building a dashboard for an e-commerce site. He wrote code to fetch the order list from the server.
// dashboard.js
const orders = fetchOrdersSync(); // Takes 5 seconds
render(orders);
showFooter();If JavaScript processed everything synchronously, what would happen? For the 5 seconds until fetchOrdersSync finishes, the browser would enter a "frozen" state. No clicking, no scrolling. Users would see this unresponsive screen and leave immediately.
To solve this problem, JavaScript adopted an Asynchronous approach. It effectively says, "I'll leave this long task here for now," and proceeds to execute the next line of code.
The Event Loop and Friends
To understand this asynchronous processing, we need to know the structure of how the JavaScript engine runs. Broadly, three elements cooperate:
And the traffic controller that coordinates these elements is the Event Loop.
Practical Mechanism: Promise vs setTimeout
Let's verify this with code. In what order will the following code be output?
// async-quiz.js
console.log('1. Start');
setTimeout(() => {
console.log('2. Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise');
});
console.log('4. End');Intuitively, since setTimeout is 0ms, it feels like it should run immediately, and the Promise resolves immediately too, making the order confusing.
The result is as follows:
1. Start
4. End
3. Promise
2. TimeoutWhy is the Promise executed before the setTimeout?
Here, a crucial concept appears. There isn't just one Task Queue.
The Microtask Queue's Priority (Queue Jumping)
JavaScript has two types of queues with different priorities.
The rule is simple. When the Call Stack is empty, the Event Loop checks the Microtask Queue first. It processes all tasks in this queue until it is empty before it fetches even a single task from the Macro Task Queue.
In other words, Promise has a higher priority than setTimeout. It's essentially a VIP line.
Application in React
How does this principle apply to React development?
1. Batching of State Updates
React 18's Automatic Batching and asynchronous state updates operate similarly to the concept of microtasks, reducing unnecessary re-renders.
2. Avoiding Heavy Tasks
It is important not to block the Event Loop.
❌ Bad Example:
// Occupies the Call Stack and freezes the browser
const handleClick = () => {
// Heavy calculation repeating 1 billion times
heavyCalculation();
setCount(c => c + 1);
};✅ Good Example:
If a heavy calculation is necessary, use a Web Worker, or break the task into smaller chunks using setTimeout to give the Event Loop a chance to breathe.
Key Takeaways
The core of JavaScript asynchronous processing is "Not Waiting" and "Priority."
Now, when writing asynchronous code, try to imagine which queue your code is lining up in. You will start to see the order of execution.
🔗 References
Next Post Teaser:
Now that we've conquered types and async, it's time to dive into the core engine of React. We use useState all the time. It seems like it simply stores values, but hidden inside is the age-old magic of JavaScript called Closures.
Continuing in: "How useState Remembers Values: Mastering Closures and Execution Context"