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)
56: The Call Stack and Task Queue
I remember working with a junior developer a few years back who was building a complex data dashboard. He had a piece of code that needed to run immediately after the DOM finished updating, so he used the classic trick: setTimeout(() => { updateUI(); }, 0);. He was completely baffled when he noticed that a console.log('Done!') placed after the setTimeout block was printing to the console before the updateUI function ever ran. He kept insisting that 0 milliseconds meant "do this right now."
This is the exact moment where most developers realize that JavaScript doesn't actually work the way it looks on the page. To understand why that console.log won the race, you have to stop thinking about the code as a linear list of instructions and start thinking about it as a system of queues and stacks.
The LIFO Nature of the Call Stack
JavaScript is single-threaded, meaning it can only do one thing at a time. It manages this using a Call Stack. Think of it like a physical stack of Pringles: the last one you put in is the first one you eat. This is called LIFO (Last In, First Out).
When you call a function, JavaScript "pushes" that function onto the top of the stack. If that function calls another function, that new one gets pushed on top of the first. Once a function returns a value, it's "popped" off the stack, and JavaScript goes back to whoever was underneath it.
function greet() {
sayHello();
console.log("Back in greet");
}
function sayHello() {
console.log("Hello!");
}
greet();
In this snippet, greet() is pushed onto the stack first. Then, because greet calls sayHello(), that gets pushed on top. Only after sayHello finishes and is popped off can the stack move back to the console.log inside greet. It's a very strict, synchronous process.
The Task Queue and the Event Loop
So, what happens when we do something that takes time, like a setTimeout or a network request? If JavaScript waited for a server to respond, your entire browser would freeze—you wouldn't even be able to click a button or scroll. To prevent this, JavaScript hands these "heavy" tasks off to the browser's Web APIs.
When a setTimeout timer expires, the callback function doesn't just jump back onto the Call Stack. If it did, it might interrupt a function that's currently mid-execution, which would be chaotic. Instead, the callback is placed into the Task Queue (also known as the Callback Queue).
This is where the Event Loop comes in. The Event Loop has one simple job: it constantly looks at the Call Stack and the Task Queue. If the Call Stack is empty, it takes the first task from the queue and pushes it onto the stack to be executed. This is why our junior developer's console.log ran first; the setTimeout callback was sitting in the queue, waiting for the stack to be completely clear, while the console.log was part of the original synchronous execution on the stack.
It's also worth noting that not all queues are created equal. Promises use a Microtask Queue, which actually has higher priority than the Task Queue. If a Promise resolves and a setTimeout expires at the exact same time, the Promise callback will always jump the line and run first. I've seen plenty of bugs caused by developers forgetting this priority shift.
📋 Practical Task
Exercise: Predicting the Execution Order
Below is a piece of code that mixes synchronous execution, a macro-task (setTimeout), and a micro-task (Promise). Your goal is to determine the exact order in which the messages will be printed to the console.
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
Promise.resolve().then(() => {
console.log("C");
});
console.log("D");
Your Task: Write down the sequence of letters (e.g., "A, B, C, D") that will appear in the console. Then, write a brief explanation of why the letters appear in that order, specifically mentioning the Call Stack, the Microtask Queue, and the Task Queue.
There are no comments for now.