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.
Start Learning`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.
const user = {
name: "Ali",
greet() {
console.log("Hello, " + this.name);
}
};
user.greet();
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.
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();
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.
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const counter = outer();
console.log(counter());
console.log(counter());
console.log(counter());
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.
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());
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.
const animal = {
eat() { return "Eating..."; }
};
const dog = Object.create(animal);
dog.bark = function() { return "Woof!"; };
console.log(dog.bark());
console.log(dog.eat());
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.
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());
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.
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());
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.
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);
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.
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));
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.
const name = "Ali";
const role = "developer";
const card = `
Name: ${name}
Role: ${role.toUpperCase()}
`;
console.log(card);
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.
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);
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.
const nums = [1, 2, 3];
let sum = 0;
nums.forEach(n => { sum += n; });
const squared = nums.map(n => n * n);
console.log(sum, squared);
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'.
function fetchData(callback) {
console.log("Fetching data...");
setTimeout(() => {
callback("Data received");
}, 1000);
}
fetchData((message) => {
console.log(message);
});
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.
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));
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.
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));
`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.
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();
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.
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then(response => response.json())
.then(data => console.log(data.title))
.catch(error => console.log("Error:", error));
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.
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");
}
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.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
`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.
const timer = setTimeout(() => {
console.log("Ran once after 1 second");
}, 1000);
const interval = setInterval(() => {
console.log("Tick");
}, 500);
setTimeout(() => clearInterval(interval), 1500);
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.
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);
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.
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"));
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.
// math.js
export function add(a, b) {
return a + b;
}
// main.js
import { add } from "./math.js";
console.log(add(5, 3));
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.
function order(item, quantity = 1, ...extras) {
return {
item,
quantity,
extras
};
}
console.log(order("Pizza"));
console.log(order("Pizza", 3, "Cheese", "Sauce"));
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.
const person = {
name: "Ali",
hobbies: ["reading", "coding"],
print() {
this.hobbies.forEach(hobby => {
console.log(this.name + " likes " + hobby);
});
}
};
person.print();
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.
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5), triple(5));
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.
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));
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.
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));
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.
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");
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.
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());
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
Step-by-step: How it works
- The user enters a GitHub username.
fetch()requests the GitHub API URL for that user.- The response is converted to JSON with
response.json(). - Destructuring pulls out name, avatar_url, bio, public_repos, and followers.
- A
try/catchblock handles missing users or network errors. - The profile details are printed to the console.
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");
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
Step-by-step: How it works
- The Cart class loads saved items from localStorage in its constructor.
add()pushes an item and saves the cart back to localStorage.remove()deletes an item and saves again.list()prints every item usingmap().total()sums all prices usingreduce().
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());
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
Step-by-step: How it works
- The user enters a city name.
fetchWeather()requests the weather API usingawait fetch().- If the response is not OK, an error is thrown and caught.
- On success, temperature and condition are destructured from the JSON.
- The last searched city is saved to localStorage.
- The result is printed with a clean, formatted message.
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}%`);
}
});