How JS Works
Part 1: Single-Threaded
JavaScript is single-threaded — it has one call stack and can only execute one piece of code at a time. There's no parallel execution of your JS code. This makes it simple to reason about, but raises a question: how does it handle slow tasks like network requests without freezing the browser?
The problem with blocking code
// If JS blocked on slow tasks, this would freeze the browser:
console.log("1. Start");
// Imagine this takes 5 seconds
let data = fetchDataSlowly(); // ← browser would freeze here!
console.log("2. Done"); // user can't click anything until this runsReal browsers don't freeze like this because JavaScript uses asynchronous execution for slow operations — and the mechanism behind it is the event loop.
Part 2: The Event Loop
The event loop is the system that allows JavaScript to be non-blocking even though it's single-threaded. It has three components working together:
Call Stack
Where your JavaScript code actually runs. Functions are pushed on when called and popped off when they return. Only one function runs at a time.
Web APIs / Node APIs
Browser (or Node.js) provides these: setTimeout, fetch, DOM events. When you call them, the browser handles the slow work in the background — not JS.
Callback Queue
When an async task completes (timer fires, data arrives), its callback is placed here, waiting for the call stack to be empty.
The loop in action
console.log("1. Start"); // → Call Stack
setTimeout(() => {
console.log("3. Timer done"); // → Web API waits 1s,
}, 1000); // then → Callback Queue
console.log("2. End"); // → Call Stack
// Output:
// 1. Start
// 2. End
// 3. Timer done ← runs after stack is emptyThe event loop constantly checks: "Is the call stack empty? Is there anything in the callback queue?"If yes to both, it moves the next callback onto the stack and runs it.
Part 3: Synchronous vs Asynchronous
Synchronous — one after another
Each line waits for the previous one to finish before running. Predictable but can block if something is slow.
console.log("A");
console.log("B");
console.log("C");
// A → B → C, always in orderAsynchronous — don't wait
Slow operations are handed off. The rest of the code keeps running. The result comes back later via a callback, promise, or async/await.
console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");
// A → C → B
// Even with 0ms, B goes through the queue!Promises and async/await — modern async
// Old way: callbacks (can get messy)
fetch(url, function(data) {
parse(data, function(result) {
save(result, function() { ... });
});
});
// Modern way: async/await (reads like sync code)
async function loadUser() {
const res = await fetch("/api/user"); // wait here
const data = await res.json(); // then here
console.log(data); // then here
}
// The await keyword pauses THIS function, but not the rest of the app