JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
57: Microtasks vs Macrotasks
A few years ago, I was reviewing a PR for a junior dev who was building a complex data dashboard. He had a bug where a "Loading" spinner would occasionally stay on the screen for a fraction of a second longer than it should, even though the data had technically arrived. He had wrapped the "hide spinner" logic in a setTimeout(() => { ... }, 0), thinking he was just pushing it to the end of the line to ensure the DOM had updated. But the data fetching was handled by a Promise. To his frustration, the spinner remained visible while other Promise-based updates were firing immediately. He kept increasing the timeout to 10ms, then 50ms, trying to "outrun" the bug without realizing he was fighting the very architecture of the JavaScript engine.
This happens because JavaScript doesn't treat all asynchronous tasks as equals. When we talk about the Event Loop, we often simplify it to "stuff that happens later," but there's actually a strict hierarchy at play: Macrotasks and Microtasks.
The Hidden Priority Queue
Think of the Macrotask queue as the main lobby of a building. This is where setTimeout, setInterval, and I/O operations wait their turn. Now, imagine a VIP side-door—that's the Microtask queue. This queue is reserved for things like Promise.then() callbacks, MutationObserver, and queueMicrotask().
Here is the crucial part: the Event Loop will not move on to the next Macrotask until the Microtask queue is completely empty. If a microtask schedules another microtask, that new one also gets processed in the same cycle. This is why Promises always seem to "jump the line" ahead of timers.
console.log('1: Script start');
setTimeout(() => {
console.log('2: setTimeout (Macrotask)');
}, 0);
Promise.resolve().then(() => {
console.log('3: Promise 1 (Microtask)');
}).then(() => {
console.log('4: Promise 2 (Microtask)');
});
console.log('5: Script end');
If you run this, you'll see 1, 5, 3, 4, and finally 2. Even though the timeout was set to 0ms, the engine finished the synchronous script, cleared out every single pending microtask (both the first and second .then()), and only then looked back at the macrotask queue to find the timeout.
The Danger of Microtask Starvation
Since the engine refuses to move to the next macrotask until the microtask queue is empty, you can actually crash your browser or freeze your UI if you aren't careful. I've seen developers accidentally create an infinite loop of microtasks—for example, a recursive function that schedules itself via queueMicrotask.
Unlike a setTimeout recursion, which allows the browser to breathe, render the page, and handle clicks between calls, a microtask loop blocks everything. The browser can't repaint the screen because it's still stuck in the "VIP side-door" trying to clear the microtask queue. If you find your UI freezing despite using asynchronous code, check if you're flooding the microtask queue.
In short: use Promises and microtasks for things that need to happen immediately after the current execution but before the browser renders. Use setTimeout when you actually want to yield control back to the browser to let it handle other events or paint the screen.
📋 Practical Task
The Async Execution Race Debugger
Below is a snippet of code intended to synchronize a state update and a logging mechanism. However, the logs are appearing in an unexpected order, causing the developer to think the state isn't updating before the log fires. Your task is to rewrite the logStatus function using queueMicrotask instead of setTimeout to ensure the log happens after the Promise resolves but before the browser handles any other pending macrotasks.
const state = { loading: true };
function updateState() {
return new Promise((resolve) => {
console.log("Updating state...");
state.loading = false;
resolve();
});
}
function logStatus() {
// CURRENTLY: This is a macrotask
setTimeout(() => {
console.log(`State loading: ${state.loading}`);
}, 0);
}
// Execution
updateState().then(() => {
console.log("State update Promise resolved.");
logStatus();
});
console.log("Script execution finished.");
// Expected output order after your fix:
// 1. Updating state...
// 2. Script execution finished.
// 3. State update Promise resolved.
// 4. State loading: false
Instructions: Modify the logStatus function to ensure the log is queued as a microtask. Verify that the output matches the expected order provided in the comments.
There are no comments for now.