Each stage leans on the one before it — closures don't make sense until you understand scope, and async doesn't make sense until you understand the call stack.
STAGE 00
How JavaScript actually runs
Week 1Most courses jump straight into syntax and skip this — which is exactly why so many self-taught developers can write code that works but can't explain why a bug is happening.
What you'll learn
- The call stack and execution context — what happens when a function runs
- Hoisting: why var, function declarations, and function expressions behave differently
- The two-pass way the engine reads your file before running it
Code
console.log(a); // undefined, not an error — why?
var a = 5;
sayHi(); // works — function declarations are fully hoisted
function sayHi() { console.log("hi"); }
sayBye(); // TypeError — sayBye is not a function
var sayBye = function() { console.log("bye"); };BuildNo app this week — take 5 hoisting/scope snippets and write down what each one logs before running it. Score yourself. This is the single highest-leverage exercise in the whole path.
Self-checkWhat does the first line log — undefined, 5, or a ReferenceError? Explain hoisting in one sentence before you scroll up to check.
STAGE 01
Values, types & coercion
Week 1–2“Just use ===” is advice you can follow without understanding. This stage makes sure you know the actual coercion rules underneath it.
What you'll learn
- Primitives vs. reference types — what “copied by value” means for objects and arrays
- Type coercion rules for +, -, and equality — not just that they exist, but why
- Template literals, and the real difference between == and ===
Code
console.log(1 + "1"); // "11" — number coerced to string
console.log("5" - 1); // 4 — string coerced to number
console.log([] + []); // "" — both arrays stringify to empty
console.log(0 == "0"); // true — coercion kicks in
console.log(0 === "0"); // false — no coercion, different typesBuildA small validator library — validatePAN(), validateIndianPhone(), validatePinCode(). Real regex, real edge cases, something you'll reuse later.
Self-checkWhy does 0 == [] evaluate to true? Trace the coercion steps by hand before looking it up.
STAGE 02
Functions & closures
Week 2–3Closures are usually taught as “a function remembers its outer variables,” which is true and useless until you build something that only works because of it.
What you'll learn
- Function declarations vs. expressions vs. arrow functions — and where this differs
- Lexical scope and closures: what's actually being “remembered,” and why
- Higher-order functions: functions that take or return other functions
Code
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// The returned function "closes over" timer —
// every call shares that same variable, across calls.BuildWrite your own debounce() and throttle() from scratch — not imported from lodash. This is the exercise that actually tests whether closures clicked.
Self-checkIf you call debounce(fn, 300) three times in quick succession, how many times does fn actually run — and when?
STAGE 03
Arrays, objects & shaping data
Week 3Almost every real frontend task is “I have data shaped like X, I need it shaped like Y.” This stage makes that fast instead of painful.
What you'll learn
- map / filter / reduce / find — and when reduce is the wrong tool
- Destructuring, spread/rest, and shallow vs. deep copy pitfalls
- Grouping, sorting, and flattening nested data
Code
const orders = [
{ city: "Pune", amount: 450 },
{ city: "Delhi", amount: 900 },
{ city: "Pune", amount: 220 },
];
const byCity = orders.reduce((acc, order) => {
acc[order.city] = (acc[order.city] || 0) + order.amount;
return acc;
}, {});
// { Pune: 670, Delhi: 900 }BuildTake a real messy dataset (weather or transit data works well) and write five data-transform functions: group by, sort by, top-N, average, flatten.
Self-checkWhen should you reach for reduce() instead of a plain for loop — and when does reduce actually make the code harder to read?
STAGE 04
The DOM & events, done right
Week 4Everyone eventually reaches for a framework — but if you've never manipulated the real DOM by hand, you won't understand what the framework is doing for you.
What you'll learn
- DOM traversal and manipulation without a framework
- Event delegation, and why it matters once a list can grow
- Accessible forms — labels, focus states, keyboard navigation
Code
// Bad: one listener per card — breaks for cards added later
document.querySelectorAll(".card").forEach(card =>
card.addEventListener("click", handleClick)
);
// Good: one listener on the parent, works for future cards too
document.querySelector(".board").addEventListener("click", (e) => {
const card = e.target.closest(".card");
if (card) handleClick(card);
});BuildA vanilla-JS Kanban board — three columns, drag cards between them with native drag events, persist state to localStorage. No framework.
Self-checkWhy exactly does the “bad” example break when a new card is added to the board after the listeners were attached?
STAGE 05
Async JavaScript
Week 5Async/await hides the event loop from you — which is great, until a bug forces you to understand it anyway. This stage builds it the historical way so the “why” sticks.
What you'll learn
- Callbacks → Promises → async/await, and why each one exists
- The event loop: microtasks vs. macrotasks (where most developers stay fuzzy)
- fetch, and error handling with try/catch
Code
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2
// Sync code first, then microtasks (Promises),
// then macrotasks (setTimeout) — even at 0ms.BuildA live weather dashboard hitting a real public API (no key required — Open-Meteo works well) with proper loading and error states, not just the happy path.
Self-checkRewrite the code above using async/await instead of .then(). Does the logged order change? Why or why not?
STAGE 06
Modules & modern tooling
Week 6This is the stage that makes every tutorial you've seen with a vite.config.js or a package.json actually make sense, instead of being magic you copy.
What you'll learn
- ES modules — import / export, default vs. named
- npm, package.json, and what a bundler like Vite is actually doing
- Environment variables, and the real difference between browser and Node
Code
// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14159;
// main.js
import { add, PI } from "./math.js";
console.log(add(2, PI));BuildRefactor the Stage 5 weather dashboard into a proper Vite project — split it into modules, add a .env for config, deploy it.
Self-checkWhat's the actual difference between a default export and a named export — and when would you reach for each?
STAGE 07
Prototypes, classes & this
Week 7class syntax hides the prototype chain underneath it. Understanding what it compiles to is what lets you debug this-binding bugs instead of guessing.
What you'll learn
- The prototype chain — what class actually is under the hood
- The four rules of this binding: implicit, explicit, new, arrow
- call, apply, and bind
Code
const user = {
name: "Asha",
greet() { console.log(`Hi, I'm ${this.name}`); },
};
const greetFn = user.greet;
greetFn(); // "Hi, I'm undefined" — lost its binding
user.greet(); // "Hi, I'm Asha" — called as a method
greetFn.call(user); // "Hi, I'm Asha" — this restored explicitlyBuildA tiny state-management library from scratch — subscribe() / publish(), ~40 lines. This teaches you what Redux or Zustand are doing underneath.
Self-checkWhy does greetFn() lose its this binding, but user.greet() doesn't?
STAGE 08
Testing & debugging like a professional
Week 8This is the stage that's usually skipped entirely in beginner paths — and it's the single biggest tell of who's actually shipped production code.
What you'll learn
- DevTools: breakpoints, conditional breakpoints, the network and performance tabs
- Writing unit tests with Vitest
- The TDD mindset — write the failing test first
Code
import { it, expect } from "vitest";
import { debounce } from "./debounce.js";
it("only calls fn once after rapid calls", async () => {
let count = 0;
const fn = debounce(() => count++, 100);
fn(); fn(); fn();
await new Promise(r => setTimeout(r, 150));
expect(count).toBe(1);
});BuildWrite real tests for the debounce, throttle, and state-library code you built in earlier stages.
Self-checkWhat's one real bug your debounce function might have that this test would catch — but clicking around manually never would?