Chapter 19 of 28 · JavaScript for Beginners — Complete Free Tutorial

Asynchronous JavaScript & the Event Loop

JavaScript is single-threaded — one call stack, one thing at a time — yet pages stay responsive while loading data. The answer is asynchrony: heavy work is handed off (to the browser), and your code continues. This chapter explains timers, callbacks, the event loop, and the ordering rules that make async output predictable.

Why async at all?

If fetching a photo took 3 seconds synchronously, the whole page would freeze for 3 seconds — no clicks, no scrolling, no animations. Asynchronous APIs start the work and give you a callback to run when the result is ready, leaving the main thread free.

setTimeout and setInterval

console.log('A: start');

setTimeout(() => {
  console.log('B: after 1000ms');
}, 1000);

const id = setInterval(() => {
  console.log('tick');
}, 500);

setTimeout(() => clearInterval(id), 1600);   // stop after ~3 ticks

console.log('C: end');

// Expected output order:
// A: start
// C: end
// tick  (×~3)
// B: after 1000ms   — and the whole tab never froze

Expected output: C prints before B — setTimeout(fn, 1000) means “at least 1000ms later, when the stack is free”, not “exactly at 1000ms”. Always keep the timer id returned by setInterval so you can clearInterval it.

The call stack, in one diagram

┌─────────────┐
│ main()      │  ← running
├─────────────┤
│ foo()       │
├─────────────┤
│ bar()       │  ← top: current function
└─────────────┘
pop when finished — an overflowing stack → RangeError

The stack must empty before the next queued callback runs. A long while loop blocks everything — rendering, clicks, timers.

The event loop (the mental model)

┌──────────────────┐   ┌──────────────────┐   ┌──────────────────┐
│   Call Stack     │   │  Web APIs        │   │  Callback Queue  │
│  (your code)     │──▶│  timers, fetch,  │──▶│  setTimeout cb,  │
│                  │◀──│  events (browser)│   │  click handlers  │
└──────────────────┘   └──────────────────┘   └────────┬─────────┘
        ▲                                              │
        └───────── event loop: stack empty? take next ─┘

  Promises go to a SEPARATE, higher-priority Microtask Queue
  (drained fully after every callback: .then, await continuations)
  1. Your code runs on the stack.
  2. The browser's APIs handle the long-running work in the background.
  3. When done, callbacks are queued — not executed immediately.
  4. The event loop moves queued callbacks onto the stack only when it is empty.

Ordering: tasks vs microtasks

console.log('1: sync');

setTimeout(() => console.log('4: timeout (task)'), 0);

Promise.resolve().then(() => console.log('3: promise (microtask)'));

console.log('2: sync');

// Expected output:
// 1: sync
// 2: sync
// 3: promise (microtask)
// 4: timeout (task)

Promises beat timers, always — even setTimeout(..., 0) loses to a microtask. This single output puzzle appears in interviews constantly; now you know why.

The famous loop + timer bug

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));   // → 3 3 3
}
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));   // → 0 1 2 ✅

Explained fully in Closures: var shares one binding; let creates a fresh one per iteration. Short version: use let.

Callback patterns you will meet

// Node-style: error first
fs.readFile(path, (err, data) => {
  if (err) return handle(err);
  use(data);
});

// Event-style
button.addEventListener('click', () => doWork());

// Debounce: collapse bursts of rapid events into one
function debounce(fn, ms = 300) {
  let t;
  return (...a) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...a), ms);
  };
}
searchInput.addEventListener('input', debounce((e) => runSearch(e.target.value), 400));

requestAnimationFrame: sync with frames

function animate() {
  pos += 2;
  box.style.transform = `translateX(${pos}px)`;
  if (pos < 300) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);   // runs before each repaint (~60fps)

Use rAF for animation (it pauses in background tabs), not setInterval.

Callback hell and the road ahead

// nested callbacks — readable enough here, painful at depth
getUser(id, (user) => {
  getOrders(user, (orders) => {
    getTotal(orders, (total) => { /* … */ });
  });
});

Two structural fixes come in the next chapters: Promises (flattening via chaining) and async/await (writing async code that reads top-to-bottom).

Blocking the thread — what to avoid

  • Heavy computation in a loop over millions of items → chunk it (setTimeout/rAF between slices) or use a Web Worker.
  • Synchronous APIs (rare in modern browsers) → always the async variant.
  • Updating the DOM thousands of times in a loop → batch (chapter 15).

Common mistakes

  • Expecting setTimeout(fn, 0) to run “immediately” — it is the next free task, after microtasks.
  • Assuming async order without waiting — code after a fetch starts before the data arrives; you must await/then.
  • Leaking intervals — setInterval without clearInterval keeps firing (even after navigation in SPAs).
  • Long sync work on the main thread — frozen UI; profile first, then chunk or offload.
  • var in timer loops — one shared binding; use let.

Practice checklist

  1. Predict the output order of a mixed sync/microtask/task snippet — then run it and explain every line.
  2. Build a 3-second countdown with setInterval that clears itself and prints “Done”.
  3. Write a debounce wrapper and log how many times a fast-typed search handler runs.
  4. Reproduce the var loop bug and fix it two ways (let and an IIFE closure).

Callbacks work — but Promises make async code composable. Next: Promises: Handling Async Operations.

Quiz: Asynchronous JavaScript & the Event Loop

5 questions · 70% to pass

Frequently Asked Questions

JavaScript is the programming language of the web. It adds behaviour to pages: responding to clicks and input, validating forms, fetching data from APIs, updating the DOM without reloading, and much more. It also runs on servers (Node.js), in desktop and mobile apps, and in build tooling.

They are unrelated languages that share a similar name for historical marketing reasons. Java is a statically-typed compiled language; JavaScript is a dynamically-typed interpreted language designed for the browser. Knowing one does not imply knowledge of the other.

Helpful but not mandatory — HTML gives you the structure JavaScript manipulates and CSS the styling it toggles. If you know basic tags, classes and selectors, you have enough to start; the DOM chapters assume only that much.

Use const by default and let when the value must change. Avoid var entirely in modern code: it is function-scoped instead of block-scoped, hoists differently, and can create accidental globals. This course uses const and let throughout.

The Document Object Model is the browser’s in-memory tree of objects representing your HTML. JavaScript reads and modifies that tree — selecting elements, changing text, adding nodes, attaching event listeners — and the browser re-renders the page to match.

== compares after converting types (so 5 == "5" is true), while === requires the same value AND the same type (5 === "5" is false). Loose equality has famously confusing rules — use === and !== everywhere unless you have a specific reason not to.

JavaScript is single-threaded, so long-running work (timers, fetches, events) is handled by the browser and its callbacks are queued. The event loop moves queued callbacks onto the call stack only when it is empty, with promise callbacks (microtasks) taking priority over timers. It is why asynchronous code never freezes the page.

A Promise is an object representing a future value — pending, fulfilled or rejected — that can be chained and combined (Promise.all). async/await is syntax sugar over promises that lets asynchronous code read top-to-bottom with ordinary try/catch error handling.

Yes. Frameworks are built on exactly these fundamentals — components are functions, state is your variable discipline, effects are events, rendering is DOM work. Developers who learn vanilla first understand what the framework is doing and debug far faster.

Type out the examples instead of copying, build small end-to-end projects (expense tracker, weather app, quiz), handle every UI state (loading, empty, success, error), and revisit fundamentals while reading real code. Consistency beats volume — a little most days outpaces occasional marathons.