SYP Start Your Path

Module 2: The Event Loop & Async Basics

Synchronous vs Asynchronous

JavaScript runs one thing at a time, in order — this is synchronous execution. But many tasks (fetching data from a server, reading a file, waiting for a timer) take time to finish. Asynchronous code lets those slow tasks happen in the background without freezing the rest of the page while they wait.

console.log("Start");
setTimeout(() => console.log("Delayed"), 1000);
console.log("End");

// Output order: "Start", "End", then "Delayed" (after ~1 second)
// The page doesn't freeze for that 1 second — other code keeps running

Call Stack & Web APIs

The call stack is where JavaScript keeps track of what function is currently running. When a function is called, it's pushed onto the stack; when it returns, it's popped off.

Slow operations like setTimeout, fetch, and DOM events aren't handled by the JavaScript engine itself — they're handed off to Web APIs provided by the browser, which run them separately and notify JavaScript once they're done.

Call Stack (JS engine)  ←→  Web APIs (browser)
     ↓ hands off timers/fetch/events
     ↓ browser runs them in the background
     ↓ browser puts the callback in a QUEUE when done
     ↓ Event Loop moves it back to the Call Stack when the stack is empty

Microtask & Macrotask Queues

Finished async work doesn't jump straight back onto the call stack — it waits in a queue. There are two, with different priority:

console.log("1");
setTimeout(() => console.log("2 (macrotask)"), 0);
Promise.resolve().then(() => console.log("3 (microtask)"));
console.log("4");

// Output: 1, 4, 3, 2
// Microtasks (Promises) always run before the next macrotask (setTimeout),
// even when the setTimeout delay is 0

The Event Loop

The event loop is the mechanism that ties this all together — it continuously checks: "Is the call stack empty? If so, take the next task from the microtask queue (drain it fully), then take ONE task from the macrotask queue, then repeat."

┌───────────────────────┐
│      Call Stack        │  ← runs your code, one thing at a time
└───────────┬─────────────┘
            │ empty? pull next task
┌───────────▼─────────────┐
│  1. Drain ALL microtasks  │  ← Promise .then callbacks
│  2. Run ONE macrotask       │  ← one setTimeout/event callback
│  3. Repeat                    │
└───────────────────────────────┘

This design is why a slow synchronous loop (a giant for loop, say) will freeze the entire page — nothing in either queue can run until the call stack is empty again.

Callbacks

The original way to handle async code — pass a function to be called once a task finishes:

function loadData(callback) {
  setTimeout(() => {
    callback("Data loaded!");
  }, 1000);
}

loadData((result) => {
  console.log(result);
});

Nesting many callbacks (each waiting on the previous) creates deeply indented, hard-to-read code often called "callback hell" — the problem Promises (next chapter) were designed to solve.