DOCODIVE
Intermediate Free Learning Path

JavaScript Intermediate Course

Level up your JavaScript with 30 intermediate lessons. Deep-dive into closures, prototypes, promises, async/await, fetch, modules, the event loop, and modern patterns — each with code, output, and official MDN docs.

4–6 weeks 30 lessons 2 projects 1 capstone Beginner knowledge required
Start Learning
01

`this` Keyword Deep Dive

The this keyword refers to the object that is currently executing the code. Its value depends on how a function is called: in a regular function it points to the global object (or undefined in strict mode), in a method it points to the owning object, and in an event handler it points to the element that received the event.

javascript
const user = {
  name: "Ali",
  greet() {
    console.log("Hello, " + this.name);
  }
};

user.greet();
The user object has a property 'name' and a method 'greet'. Inside greet(), this refers to the object that called the method — in this case user. So this.name resolves to "Ali", and the greeting prints that name.
console
Hello, Ali
Because greet() was called with user.greet(), the this keyword inside the method pointed to user. That is why this.name returned "Ali" instead of being undefined.
02

Call, Apply, Bind

call(), apply(), and bind() let you control what this points to when a function runs. call() invokes the function immediately with arguments passed individually. apply() is the same but takes arguments as an array. bind() returns a new function with this locked, without running it yet.

javascript
function intro(city, country) {
  console.log(this.name + " from " + city + ", " + country);
}

const person = { name: "Sara" };

intro.call(person, "Lahore", "Pakistan");
intro.apply(person, ["Karachi", "Pakistan"]);

const bound = intro.bind(person, "Islamabad", "Pakistan");
bound();
call() runs intro immediately with this = person and arguments "Lahore" and "Pakistan". apply() does the same but receives the arguments as an array. bind() creates a new function 'bound' with this fixed to person and the arguments pre-filled, then bound() runs it.
console
Sara from Lahore, Pakistan Sara from Karachi, Pakistan Sara from Islamabad, Pakistan
All three calls printed the person's name because call, apply, and bind all set this to the person object. The difference is only in how the arguments are passed and whether the function runs immediately.
03

Closures (Part 1 — Basics)

A closure is created when a function remembers the variables from the scope where it was created, even after that outer function has finished running. This happens because inner functions keep a reference to their outer scope.

javascript
function outer() {
  let count = 0;
  return function inner() {
    count++;
    return count;
  };
}

const counter = outer();
console.log(counter());
console.log(counter());
console.log(counter());
outer() declares a local variable count and returns the inner function. Even though outer() has finished, the returned inner function still remembers 'count' because of closure. Each time counter() is called, count increases and the new value is returned.
console
1 2 3
The counter function kept access to the count variable between calls. This proves the closure preserved the variable — count was not reset to 0 each time, because the inner function 'closed over' it.
04

Closures (Part 2 — Real Use Cases)

Closures are used everywhere in real JavaScript: creating private variables, building factories, and managing state without global variables. A common pattern is a function that returns an object of methods, all sharing the same private data.

javascript
function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) { balance += amount; return balance; },
    withdraw(amount) {
      if (amount <= balance) {
        balance -= amount;
        return balance;
      }
      return "Insufficient funds";
    },
    getBalance() { return balance; }
  };
}

const account = createBankAccount(100);
console.log(account.deposit(50));
console.log(account.withdraw(30));
console.log(account.getBalance());
createBankAccount() creates a private 'balance' variable. It returns an object with deposit, withdraw, and getBalance methods. All three methods share the same balance variable through closure, so the balance is hidden from outside and can only be changed through the methods.
console
150 120 120
Deposit added 50 to the initial 100, giving 150. Withdraw removed 30, giving 120. getBalance confirmed the final balance. The balance variable was never accessible from outside — only through the returned methods, demonstrating private state via closure.
05

Prototypes & Prototype Chain

Every JavaScript object has a hidden link to another object called its prototype. When you access a property that the object itself does not have, JavaScript looks up the prototype chain until it finds the property or reaches null. This is how objects inherit methods.

javascript
const animal = {
  eat() { return "Eating..."; }
};

const dog = Object.create(animal);
dog.bark = function() { return "Woof!"; };

console.log(dog.bark());
console.log(dog.eat());
animal is a normal object with an eat method. Object.create(animal) creates dog with animal as its prototype. dog has its own bark method, but eat is not on dog — JavaScript finds it by walking up the prototype chain to animal.
console
Woof! Eating...
dog.bark() used the method defined directly on dog. dog.eat() was not directly on dog, so JavaScript looked at dog's prototype (animal) and found eat() there. This is the prototype chain in action.
06

Classes & Constructor

Classes are a cleaner syntax for creating objects with shared methods. The constructor() method runs automatically when a new instance is created with the new keyword and is used to set up initial properties.

javascript
class Car {
  constructor(brand, model) {
    this.brand = brand;
    this.model = model;
  }

  info() {
    return this.brand + " " + this.model;
  }
}

const myCar = new Car("Toyota", "Corolla");
console.log(myCar.info());
The Car class has a constructor that receives brand and model and assigns them to this. The info() method returns both values combined. new Car("Toyota", "Corolla") creates an instance and runs the constructor, then info() is called on it.
console
Toyota Corolla
The constructor stored the brand and model on the new object. The info() method then read those stored properties and returned the combined string. This is the standard class pattern.
07

Inheritance & `extends`

A class can inherit from another class using the extends keyword. The child class gets all the parent's methods, and it can add its own or override them. The super() function calls the parent's constructor from the child.

javascript
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    return this.name + " makes a sound.";
  }
}

class Dog extends Animal {
  speak() {
    return this.name + " barks.";
  }
}

const d = new Dog("Rex");
console.log(d.speak());
Animal has a constructor that sets name, and a speak method. Dog extends Animal, inheriting the constructor. Dog overrides speak() with its own version that says the dog barks. new Dog("Rex") creates an instance, and speak() uses the Dog version.
console
Rex barks.
Because Dog overrode the speak method, the Dog version ran instead of the Animal version. The name property was still set by the inherited Animal constructor. This shows how inheritance lets a child class customize behavior.
08

Destructuring (Arrays & Objects)

Destructuring lets you unpack values from arrays or properties from objects into separate variables in one line. It makes code shorter and clearer when working with structured data.

javascript
const colors = ["red", "green", "blue"];
const [first, second] = colors;

const user = { name: "Ali", age: 25 };
const { name, age } = user;

console.log(first, second);
console.log(name, age);
The array destructuring pulls the first two values into first and second. The object destructuring pulls the name and age properties into variables with the same names. console.log then prints both pairs.
console
red green Ali 25
Array destructuring assigned 'red' to first and 'green' to second in order. Object destructuring matched by property name, so name became 'Ali' and age became 25. Both worked in a single line each.
09

Spread & Rest Operators

The ... operator does two jobs. As spread, it expands an array or object into individual elements. As rest, it collects multiple arguments or remaining items into an array. Both use the same syntax but in different positions.

javascript
const nums = [1, 2, 3];
const more = [...nums, 4, 5];

function sum(...args) {
  return args.reduce((a, b) => a + b, 0);
}

console.log(more);
console.log(sum(1, 2, 3, 4));
The spread syntax ...nums expands [1,2,3] into individual values inside the new array, then 4 and 5 are appended. The sum function uses rest parameters ...args to collect all arguments into an array, then reduce adds them together.
console
[1, 2, 3, 4, 5] 10
Spread copied the original three numbers and added two more, creating a five-element array. Rest collected 1, 2, 3, 4 into an array and reduce summed them to 10. Same ..., two different uses.
10

Template Literals Advanced

Template literals do more than insert variables. They support multi-line strings without escape characters, and tagged templates let you process a template with a custom function.

javascript
const name = "Ali";
const role = "developer";

const card = `
  Name: ${name}
  Role: ${role.toUpperCase()}
`;

console.log(card);
The backtick string spans multiple lines without \n escapes. ${name} inserts the name, and ${role.toUpperCase()} runs a method inside the interpolation and inserts "DEVELOPER". The result is stored in card and printed.
console
Name: Ali Role: DEVELOPER
The template preserved the exact line breaks and spacing. The first interpolation inserted the name, and the second called toUpperCase() on the role, proving you can evaluate expressions — not just variables — inside ${}.
11

map, filter, reduce Deep Dive

These three array methods are the foundation of functional-style JavaScript. map() transforms every element. filter() keeps only elements that pass a test. reduce() combines all elements into a single value.

javascript
const prices = [10, 20, 30, 40];

const doubled = prices.map(p => p * 2);
const affordable = prices.filter(p => p <= 30);
const total = prices.reduce((sum, p) => sum + p, 0);

console.log(doubled, affordable, total);
map() multiplies every price by 2, producing a new array. filter() keeps prices less than or equal to 30. reduce() starts with 0 and adds every price, returning the total. None of the three methods mutate the original prices array.
console
[20, 40, 60, 80] [10, 20, 30] 100
doubled contains each price multiplied by 2. affordable contains only 10, 20, and 30. total is 100, the sum of all prices. All three operations produced new values without changing the original array.
12

forEach vs map vs for

All three iterate over arrays, but they differ. A for loop gives you full control with an index. forEach runs a function for each item but returns nothing. map also runs a function for each item but returns a new array of results.

javascript
const nums = [1, 2, 3];

let sum = 0;
nums.forEach(n => { sum += n; });

const squared = nums.map(n => n * n);

console.log(sum, squared);
forEach adds each number to sum using side effects; it returns undefined. map creates a new array 'squared' where each number is multiplied by itself. The original nums array is unchanged in both cases.
console
6 [1, 4, 9]
sum became 6 because forEach accumulated the values. squared contains the square of each original number. This shows the key difference: use forEach for side effects and map when you need a transformed array back.
13

Callback Functions

A callback is a function passed into another function as an argument, to be executed later. Callbacks are the foundation of asynchronous JavaScript — they let you say 'when this finishes, run this other code'.

javascript
function fetchData(callback) {
  console.log("Fetching data...");
  setTimeout(() => {
    callback("Data received");
  }, 1000);
}

fetchData((message) => {
  console.log(message);
});
fetchData accepts a callback function. It logs "Fetching data...", then after 1 second setTimeout runs the callback with the string "Data received". The callback receives that message and logs it.
console
Fetching data... Data received
The first line printed immediately. One second later, the callback executed and printed "Data received". This shows how a callback runs later, after the simulated delay, rather than blocking the rest of the program.
14

Promises (Part 1 — Basics)

A Promise represents a value that will be available in the future. It has three states: pending, fulfilled, and rejected. You create a promise with new Promise() and handle its result with .then() for success and .catch() for errors.

javascript
const promise = new Promise((resolve, reject) => {
  const success = true;
  setTimeout(() => {
    if (success) {
      resolve("Operation succeeded");
    } else {
      reject("Operation failed");
    }
  }, 1000);
});

promise
  .then(message => console.log(message))
  .catch(error => console.log(error));
The Promise runs an executor function with resolve and reject. After 1 second, because success is true, it calls resolve() with a success message. .then() handles the resolved value, and .catch() would handle a rejection if it happened.
console
Operation succeeded
The promise resolved after one second, so .then() ran and printed the success message. .catch() did not run because there was no error. This is the basic resolve/reject pattern.
15

Promises (Part 2 — Chaining & Errors)

Promises can be chained: each .then() returns a new promise, so you can run steps in order. If any step throws, control jumps to the nearest .catch(). This avoids nested callback pyramids.

javascript
function step(value) {
  return new Promise(resolve => {
    setTimeout(() => resolve(value * 2), 500);
  });
}

step(2)
  .then(result => step(result))
  .then(result => step(result))
  .then(final => console.log("Final:", final))
  .catch(err => console.log("Error:", err));
step() returns a promise that doubles its input after half a second. The chain starts with step(2), then each .then passes the result into the next step. The final .then prints the last value. .catch would handle any failure in the chain.
console
Final: 16
The value doubled three times: 2 became 4, then 8, then 16. Each .then waited for the previous promise to resolve. The clean chain shows how promise chaining avoids deeply nested callbacks.
16

`async` / `await`

async and await are syntactic sugar over promises. Marking a function async makes it return a promise. Inside it, await pauses execution until a promise resolves, making asynchronous code read like synchronous code.

javascript
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function run() {
  console.log("Start");
  await delay(1000);
  console.log("End after 1 second");
}

run();
delay() returns a promise that resolves after ms milliseconds. run() is async, so it can use await. It logs "Start", waits for delay(1000) to resolve, then logs the end message. The await keyword pauses only inside run(), not the whole program.
console
Start End after 1 second
"Start" printed immediately. After one second, "End after 1 second" printed. The await made the code wait for the promise before continuing, but the syntax stayed flat and readable — that is the main advantage of async/await.
17

Fetch API & HTTP

fetch() is the modern way to make HTTP requests. It returns a promise that resolves to a Response object. You call response.json() to parse JSON data. It handles GET, POST, and other HTTP methods.

javascript
fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then(response => response.json())
  .then(data => console.log(data.title))
  .catch(error => console.log("Error:", error));
fetch() sends a GET request to the URL and returns a promise. The first .then receives the Response and calls response.json() to parse it. The second .then receives the parsed object and prints its title property. .catch handles network errors.
console
delectus aut autem
The API returned a JSON todo object. The title property of that object is 'delectus aut autem', which was printed. This demonstrates a complete fetch flow: request, parse, and use the data.
18

Error Handling (try/catch/finally)

try/catch lets you handle errors gracefully instead of crashing. Code in try runs first; if it throws, catch handles the error. finally always runs whether there was an error or not, making it ideal for cleanup.

javascript
function divide(a, b) {
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }
  return a / b;
}

try {
  console.log(divide(10, 0));
} catch (error) {
  console.log("Caught:", error.message);
} finally {
  console.log("Done");
}
divide() throws a new Error when b is zero. The try block calls divide(10, 0), which throws. catch receives the error and prints its message. finally runs after either try or catch, printing "Done".
console
Caught: Cannot divide by zero Done
The error was thrown inside divide and caught in the catch block, so the program did not crash. The message was printed, and finally ran last. This is proper error handling.
19

Event Loop & Microtasks

The event loop is how JavaScript handles asynchronous code on a single thread. Synchronous code runs first, then microtasks (promises), then macrotasks (setTimeout). This ordering explains why some callbacks run before others.

javascript
console.log("A");

setTimeout(() => console.log("B"), 0);

Promise.resolve().then(() => console.log("C"));

console.log("D");
"A" and "D" are synchronous and print immediately in order. setTimeout schedules a macrotask for "B". Promise.resolve().then schedules a microtask for "C". Microtasks run before macrotasks, even though setTimeout has 0 delay.
console
A D C B
Synchronous code (A, D) ran first. Then the microtask queue (C) was emptied before the macrotask queue (B). This ordering — sync, then microtasks, then macrotasks — is the core of the event loop.
20

`setTimeout` & `setInterval`

setTimeout runs a function once after a delay. setInterval runs a function repeatedly at a fixed interval. Both return an ID that you can pass to clearTimeout or clearInterval to cancel them.

javascript
const timer = setTimeout(() => {
  console.log("Ran once after 1 second");
}, 1000);

const interval = setInterval(() => {
  console.log("Tick");
}, 500);

setTimeout(() => clearInterval(interval), 1500);
The first setTimeout prints once after 1 second. setInterval prints "Tick" every 500ms. A final setTimeout clears the interval after 1.5 seconds, stopping the ticks. The timer variable holds the timeout ID, and interval holds the interval ID.
console
Tick Tick Ran once after 1 second Tick
"Tick" appeared roughly every half second, the one-time message appeared at one second, and the ticking stopped at 1.5 seconds because clearInterval cancelled it. This shows timed one-shot and repeating execution.
21

JSON (parse, stringify)

JSON is a text format for exchanging data. JSON.stringify() converts a JavaScript object into a JSON string. JSON.parse() converts a JSON string back into a JavaScript object. These two are how data travels between servers and browsers.

javascript
const user = { name: "Ali", age: 25 };

const jsonString = JSON.stringify(user);
const parsed = JSON.parse(jsonString);

console.log(jsonString);
console.log(parsed.name, parsed.age);
JSON.stringify() converts the user object to the text '{"name":"Ali","age":25}'. JSON.parse() converts that text back into an object assigned to parsed. Then parsed.name and parsed.age access the restored values.
console
{"name":"Ali","age":25} Ali 25
The stringified output is the JSON text representation of the object. The parsed object has the same properties as the original, so parsed.name returned "Ali" and parsed.age returned 25. This is a full round-trip conversion.
22

LocalStorage & SessionStorage

Both store key-value pairs in the browser as strings. localStorage persists even after the browser closes. sessionStorage only lasts for the current tab session. Use setItem to save, getItem to read, and removeItem to delete.

javascript
localStorage.setItem("theme", "dark");
localStorage.setItem("language", "en");

console.log(localStorage.getItem("theme"));
console.log(localStorage.length);

localStorage.removeItem("theme");
console.log(localStorage.getItem("theme"));
setItem stores two keys. getItem('theme') reads the theme value, and localStorage.length returns how many items exist. removeItem('theme') deletes that key, and the final getItem returns null because the key no longer exists.
console
dark 2 null
The first output was the stored theme 'dark'. The length was 2 because two items existed. After removal, getItem returned null since 'theme' was deleted. This shows the complete storage lifecycle.
23

ES Modules (import/export)

ES modules let you split code into separate files. You export functions or values from one file and import them into another. This keeps code organized and reusable. Modules use the .mjs extension or type='module' script tags.

javascript
// math.js
export function add(a, b) {
  return a + b;
}

// main.js
import { add } from "./math.js";

console.log(add(5, 3));
In math.js, the add function is exported so other files can use it. In main.js, import { add } pulls that function in. Then add(5, 3) is called and logged. This is the named export/import pattern.
console
8
The add function defined in math.js was imported into main.js and called successfully, returning 8. This proves modules share code across files cleanly.
24

Default & Rest Parameters

Default parameters give a fallback value when an argument is missing. Rest parameters collect all remaining arguments into an array, making functions flexible with any number of inputs.

javascript
function order(item, quantity = 1, ...extras) {
  return {
    item,
    quantity,
    extras
  };
}

console.log(order("Pizza"));
console.log(order("Pizza", 3, "Cheese", "Sauce"));
The order function has a default quantity of 1. The ...extras rest parameter collects any extra arguments after quantity into an array. The first call uses the default quantity. The second call overrides it and passes two extras.
console
{ item: 'Pizza', quantity: 1, extras: [] } { item: 'Pizza', quantity: 3, extras: ['Cheese', 'Sauce'] }
The first call had no quantity, so the default 1 was used and extras was empty. The second call set quantity to 3 and collected two extras into the array. This shows both features working together.
25

Arrow Functions Deep Dive

Arrow functions are shorter than regular functions and have a key difference: they do not have their own this. Instead, they inherit this from the surrounding scope. This makes them ideal for callbacks but unsuitable for methods that need dynamic this.

javascript
const person = {
  name: "Ali",
  hobbies: ["reading", "coding"],
  print() {
    this.hobbies.forEach(hobby => {
      console.log(this.name + " likes " + hobby);
    });
  }
};

person.print();
print() is a regular method, so this refers to person. Inside forEach, an arrow function is used, so it inherits this from print()'s scope — meaning this still points to person. If a regular function were used instead, this would be lost.
console
Ali likes reading Ali likes coding
The arrow function kept this pointing to person, so this.name correctly resolved to "Ali" for each hobby. This is why arrow functions are the default choice for callbacks.
26

Higher-Order Functions

A higher-order function either takes a function as an argument or returns a function. This is a core functional programming concept. It enables patterns like function factories, decorators, and callback-driven code.

javascript
function multiplyBy(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5), triple(5));
multiplyBy returns a new function that multiplies its input by the captured factor. double is a function that multiplies by 2, and triple multiplies by 3. Both are then called with 5.
console
10 15
double(5) returned 10 (5 × 2) and triple(5) returned 15 (5 × 3). The same higher-order function created two different specialized functions, showing the factory pattern.
27

Array Sorting & Custom Comparators

sort() sorts an array in place. By default it sorts as strings, which gives wrong results for numbers. You fix this by passing a comparator function that returns a negative, zero, or positive number to define the order.

javascript
const nums = [40, 100, 1, 5];

nums.sort((a, b) => a - b);
console.log(nums);

const people = [{ name: "Ali", age: 30 }, { name: "Sara", age: 25 }];
people.sort((a, b) => a.age - b.age);
console.log(people.map(p => p.name));
The first sort uses the comparator (a, b) => a - b to sort numbers ascending. The second sorts objects by their age property using the same pattern. map then extracts just the names from the sorted array.
console
[1, 5, 40, 100] ['Sara', 'Ali']
The numbers are correctly sorted from smallest to largest because the comparator subtracts. The people are sorted by age, so Sara (25) comes before Ali (30). Custom comparators give you full control over sorting.
28

Set & Map

Set stores unique values — duplicates are automatically ignored. Map stores key-value pairs where keys can be any type, unlike objects which convert keys to strings. Both have useful methods like add, set, get, has, and delete.

javascript
const set = new Set([1, 2, 2, 3]);
set.add(4);

const map = new Map();
map.set("name", "Ali");
map.set(1, "one");

console.log(set, set.has(2));
console.log(map.get("name"), map.get(1));
The Set is created with [1,2,2,3] but stores only unique values, so 2 appears once. add(4) adds a new value. The Map stores a string key 'name' and a number key 1. get() retrieves both values.
console
Set { 1, 2, 3, 4 } true Ali one
The Set shows unique values only and has(2) returns true. The Map returned 'Ali' for the string key and 'one' for the number key. This shows Set's deduplication and Map's flexible keys.
29

Debouncing & Throttling

Debouncing delays a function until the user stops triggering it for a set time — perfect for search inputs. Throttling limits a function to run at most once per interval — perfect for scroll and resize events. Both improve performance.

javascript
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const log = debounce((value) => console.log("Search:", value), 300);

log("a");
log("ab");
log("abc");
debounce returns a wrapper that clears any existing timer and starts a new one each time it is called. When log() is called three times quickly, only the last call survives because each call resets the 300ms timer. After 300ms of silence, the last value runs.
console
Search: abc
Only the final call with "abc" executed because the first two were cancelled by clearTimeout before the delay elapsed. This is exactly what debouncing does — it waits for the user to stop, then runs once.
30

Intermediate Recap + 2 Projects

You now know this binding, closures, prototypes, classes, inheritance, destructuring, spread/rest, promises, async/await, fetch, error handling, the event loop, modules, and performance patterns. Apply them in two projects: (1) GitHub User Finder using fetch; (2) Shopping Cart using classes and localStorage.

javascript
class ShoppingCart {
  constructor() {
    this.items = [];
  }

  add(item, price) {
    this.items.push({ item, price });
  }

  total() {
    return this.items.reduce((sum, entry) => sum + entry.price, 0);
  }
}

const cart = new ShoppingCart();
cart.add("Book", 15);
cart.add("Pen", 3);
console.log(cart.total());
ShoppingCart is a class with an items array. add() pushes an object containing the item name and price. total() uses reduce to sum all prices. A cart instance is created, two items are added, and the total is printed.
console
18
The total method added 15 and 3, returning 18. This demonstrates classes, arrays, objects, and reduce working together — exactly the combination you need for real projects.

JavaScript Intermediate Projects

Apply closures, promises, fetch, and classes. Click each project to open its full guide.

About this project

Enter a GitHub username and fetch that user's profile using the GitHub API. Display the name, avatar, bio, public repos, and followers using async/await and fetch.

What you'll practice
fetch() async / await JSON destructuring error handling
Step-by-step: How it works
  1. The user enters a GitHub username.
  2. fetch() requests the GitHub API URL for that user.
  3. The response is converted to JSON with response.json().
  4. Destructuring pulls out name, avatar_url, bio, public_repos, and followers.
  5. A try/catch block handles missing users or network errors.
  6. The profile details are printed to the console.
javascript
async function findUser(username) {
    try {
        const response = await fetch(`https://api.github.com/users/${username}`);

        if (!response.ok) {
            throw new Error("User not found");
        }

        const { name, avatar_url, bio, public_repos, followers } = await response.json();

        console.log("Name:", name || "N/A");
        console.log("Avatar:", avatar_url);
        console.log("Bio:", bio || "No bio");
        console.log("Public Repos:", public_repos);
        console.log("Followers:", followers);
    } catch (error) {
        console.log("Error:", error.message);
    }
}

findUser("octocat");
Sample Run
console
Name: The Octocat Avatar: https://avatars.githubusercontent.com/u/583231?v=4 Bio: No bio Public Repos: 8 Followers: 12089

About this project

Build a shopping cart that survives page reloads. Items are stored in localStorage, and the cart supports adding, removing, listing, and calculating the total using classes and array methods.

What you'll practice
classes localStorage JSON.parse/stringify reduce() map()
Step-by-step: How it works
  1. The Cart class loads saved items from localStorage in its constructor.
  2. add() pushes an item and saves the cart back to localStorage.
  3. remove() deletes an item and saves again.
  4. list() prints every item using map().
  5. total() sums all prices using reduce().
javascript
class Cart {
    constructor(key = "cart") {
        this.key = key;
        this.items = JSON.parse(localStorage.getItem(key)) || [];
    }

    save() {
        localStorage.setItem(this.key, JSON.stringify(this.items));
    }

    add(name, price) {
        this.items.push({ name, price });
        this.save();
    }

    remove(name) {
        this.items = this.items.filter(item => item.name !== name);
        this.save();
    }

    list() {
        return this.items.map(item => `${item.name} - $${item.price}`);
    }

    total() {
        return this.items.reduce((sum, item) => sum + item.price, 0);
    }
}

const cart = new Cart();
cart.add("Book", 15);
cart.add("Pen", 3);
console.log(cart.list());
console.log("Total:", cart.total());
Sample Run
console
["Book - $15", "Pen - $3"] Total: 18

Intermediate Capstone Project

Combine async/await, fetch, error handling, classes, and localStorage into one complete project.

About this project

Build a weather dashboard that fetches live weather data for a city, displays temperature and conditions, caches the last search in localStorage, and handles invalid cities gracefully.

What you'll practice
async / await fetch() try/catch localStorage JSON
Step-by-step: How it works
  1. The user enters a city name.
  2. fetchWeather() requests the weather API using await fetch().
  3. If the response is not OK, an error is thrown and caught.
  4. On success, temperature and condition are destructured from the JSON.
  5. The last searched city is saved to localStorage.
  6. The result is printed with a clean, formatted message.
javascript
const API_KEY = "YOUR_API_KEY";

async function fetchWeather(city) {
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`;

    try {
        const response = await fetch(url);

        if (!response.ok) {
            throw new Error("City not found");
        }

        const data = await response.json();
        const { main, weather } = data;

        const result = {
            city: data.name,
            temp: main.temp,
            feels_like: main.feels_like,
            condition: weather[0].description,
            humidity: main.humidity
        };

        localStorage.setItem("lastCity", result.city);
        return result;
    } catch (error) {
        console.log("Error:", error.message);
        return null;
    }
}

fetchWeather("London").then(result => {
    if (result) {
        console.log(`${result.city}: ${result.temp}°C, ${result.condition}`);
        console.log(`Feels like: ${result.feels_like}°C, Humidity: ${result.humidity}%`);
    }
});
Sample Run
console
London: 15.2°C, light rain Feels like: 13.8°C, Humidity: 81%
You've completed all 30 lessons. Ready to continue?

Continue to JavaScript Advanced and Practice resources.

📱 Scan this QR code with your phone camera to instantly open this page.

Works on iOS, Android, and any modern device. No app installation required.

Account Verified!

Your email has been verified successfully.