Complete JavaScript Tutorial

Master JavaScript — the programming language of the web.

Getting Started with JavaScript

Set up your environment, run your first scripts, and build confidence in both browser and Node.js contexts.

JavaScript runs everywhere: directly in the browser for UI work, and on the server with Node.js for tooling and APIs. This section walks through the essential setup, execution paths, and debugging habits that underpin productive learning.

Setting Up a Clean HTML Playground

Create a minimal HTML document so you always know where your JavaScript executes. Keep structure, styles, and scripts organized from day one.

Basic HTML Shell



  
  
  JS Playground
  


  

Welcome to JavaScript

Loading...

Inline vs External Scripts

Inline scripts are quick for experiments, while external files keep production code organized and cacheable. Always prefer defer for external files so HTML can finish parsing first.

Defer External Script



main.js Contents
// main.js
// The "defer" attribute ensures the DOM is parsed before this executes
document.addEventListener('DOMContentLoaded', () => {
  const banner = document.querySelector('h1');
  banner.textContent = 'JavaScript is connected!';
});

Talking to the Console

The browser console is your first debugging companion. Use it to log, group, and trace the flow of your code.

Console Logging Patterns
console.log('Simple message');
console.info('Informational', { env: 'dev' });
console.warn('Warning: slow network');
console.error('Error: missing token');
console.group('Auth Flow');
console.log('Fetching user');
console.log('Validating session');
console.groupEnd();

Your First Browser Programs

Build tiny, interactive snippets that respond to user actions. Use DOM queries and event listeners to change content without reloading.

Click to Update Text

Waiting...

Keyboard Listener
document.addEventListener('keydown', event => {
  console.log(`Key pressed: ${event.key}`);
});

Browser DevTools Essentials

Open DevTools (F12 or Ctrl+Shift+I) to inspect elements, view network calls, and profile performance. Use the Sources panel to explore files and set breakpoints.

Live Editing in DevTools
// Paste into the console to experiment without editing files
const el = document.querySelector('#status');
el.style.color = '#0a8754';
el.textContent = 'Live edit success!';
Performance Snapshot
// Quick timer for code you want to measure
console.time('loop');
for (let i = 0; i < 100000; i++) {
  Math.sqrt(i);
}
console.timeEnd('loop');

Debugging with Breakpoints

Set breakpoints in the Sources tab to pause execution. Step through code to inspect variables and watch expressions.

Manual Debugging
function greet(user) {
  const message = `Welcome, ${user}`;
  debugger; // Execution pauses here when DevTools is open
  console.log(message);
}

greet('Amina');

Fetching Data in the Browser

Use the Fetch API to retrieve data asynchronously. Combine async/await with error handling to keep UX resilient.

Simple Fetch with Async/Await
async function loadQuote() {
  const status = document.getElementById('status');
  status.textContent = 'Loading...';

  try {
    const response = await fetch('https://api.quotable.io/random');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    status.textContent = data.content;
  } catch (error) {
    console.error(error);
    status.textContent = 'Could not load quote. Please retry.';
  }
}

document.addEventListener('DOMContentLoaded', loadQuote);

Node.js Quick Start for Tooling

Node.js lets you run JavaScript outside the browser. Use it for build tools, scripts, or quick experiments without HTML.

Install and Run a Node Script
# Verify Node and npm
node -v
npm -v

# Run a simple script
node hello.js
hello.js
// hello.js
console.log('Running in Node.js');

// Access filesystem with fs (built-in module)
const fs = require('fs');
fs.writeFileSync('log.txt', 'Hello from Node at ' + new Date().toISOString());

Handling Errors Gracefully

Use try/catch around risky operations. Provide user-friendly feedback while logging detailed errors for yourself.

Robust Error Handling
function parseSettings(jsonString) {
  try {
    const settings = JSON.parse(jsonString);
    return settings;
  } catch (error) {
    console.error('Could not parse settings', error);
    return { theme: 'light', retries: 0 };
  }
}

const config = parseSettings('{ "theme": "dark" }');
console.log(config.theme);

Practice Exercises

  1. Create a Starter Page: Build a minimal HTML page with a deferred external script and verify it updates a paragraph.
  2. Console Reporter: Log grouped console messages for a mock sign-in flow and include timing with console.time.
  3. Click Counter: Add a button that increments a visible counter and logs the total to the console.
  4. Fetch and Render: Fetch JSON from a public API and render one field into the DOM with error handling.
  5. Node Hello: Write a Node script that reads a local file and prints its contents, handling missing-file errors.
  6. Debug Session: Set a breakpoint in a simple function, step through it, and note each variable value.
Key Takeaways:
  • Use clean HTML shells and the defer attribute to keep scripts predictable.
  • Rely on the console for quick feedback, grouping, and timing.
  • Attach event listeners to make pages interactive without reloads.
  • Fetch data with async/await and handle network failures gracefully.
  • Open DevTools early to inspect elements, watch variables, and set breakpoints.
  • Leverage Node.js to run JavaScript outside the browser for tooling and scripts.
  • Wrap risky operations in try/catch to keep user experiences stable.
  • Iterate quickly: edit, refresh, observe, and refine.

What's Next?

Continue with variables and control flow in the next sections to write more expressive programs, then explore DOM manipulation patterns to build interactive interfaces.

Introduction to JavaScript

Discover the world's most popular programming language for the web

JavaScript is a high-level, interpreted programming language that adds interactivity and dynamic behavior to websites. It's the third pillar of web development alongside HTML and CSS, running in every modern web browser and increasingly on servers via Node.js.

What is JavaScript?

JavaScript is a versatile, lightweight, prototype-based language that enables developers to create dynamic, interactive web experiences.

JavaScript in Action
<!DOCTYPE html>
<html>
<head>
    <title>JavaScript Demo</title>
</head>
<body>
    <h1 id="greeting">Welcome!</h1>
    <button id="changeBtn">Click Me</button>

    <script>
        // Get element references
        const greeting = document.getElementById('greeting');
        const button = document.getElementById('changeBtn');

        // Add click event listener
        button.addEventListener('click', () => {
            greeting.textContent = 'Hello, JavaScript!';
            greeting.style.color = '#3b82f6';
        });
    </script>
</body>
</html>

Key Characteristics

Understanding JavaScript's fundamental traits helps you write better code.

Language Features
// 1. Dynamic Typing
let value = 42;           // Number
value = 'Hello';          // Now a String
value = true;             // Now a Boolean

// 2. First-Class Functions
const greet = function(name) {
    return `Hello, ${name}!`;
};

const sayHello = greet;   // Functions are values
console.log(sayHello('World')); // "Hello, World!"

// 3. Prototype-Based Inheritance
const animal = {
    eat() { console.log('Eating...'); }
};

const dog = Object.create(animal);
dog.bark = function() { console.log('Woof!'); };

dog.eat();  // Inherited from animal
dog.bark(); // Own method

// 4. Event-Driven & Asynchronous
setTimeout(() => {
    console.log('Delayed execution');
}, 1000);

fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => console.log(data));

// 5. Single-Threaded with Event Loop
console.log('First');
setTimeout(() => console.log('Second'), 0);
console.log('Third');
// Output: First, Third, Second

Where JavaScript Runs

JavaScript has evolved from a browser-only language to a universal runtime.

JavaScript Environments
// 1. Browser (Client-Side)
// - DOM manipulation
document.querySelector('.btn').addEventListener('click', handleClick);

// - Browser APIs
localStorage.setItem('theme', 'dark');
navigator.geolocation.getCurrentPosition(success, error);

// - Fetch API
fetch('/api/users').then(res => res.json());

// 2. Node.js (Server-Side)
// - File system access
const fs = require('fs');
fs.readFileSync('data.txt', 'utf8');

// - HTTP servers
const http = require('http');
http.createServer((req, res) => {
    res.end('Hello from Node.js!');
}).listen(3000);

// 3. Mobile Apps
// - React Native
import { View, Text } from 'react-native';

// - Ionic
// - NativeScript

// 4. Desktop Apps
// - Electron (VS Code, Slack, Discord)
const { app, BrowserWindow } = require('electron');

// 5. IoT & Embedded
// - Johnny-Five (Arduino)
// - Tessel
// - Raspberry Pi

// 6. Game Development
// - Phaser
// - Three.js (3D graphics)

JavaScript vs Other Languages

See how JavaScript compares to other popular programming languages.

Comparison Overview
/* JavaScript Strengths:
 * ✓ Runs everywhere (browser, server, mobile, desktop)
 * ✓ Huge ecosystem (npm - 2M+ packages)
 * ✓ Easy to start, no compilation needed
 * ✓ Asynchronous by nature
 * ✓ JSON is native
 * ✓ Functional programming support
 * ✓ Active community and constant evolution
 */

// JavaScript: Event-driven & asynchronous
fetch('/api/data')
    .then(res => res.json())
    .then(data => updateUI(data));

// Python: Synchronous by default (unless async/await)
// import requests
// response = requests.get('/api/data')
// data = response.json()

// Java: Verbose, strongly typed
// HttpClient client = HttpClient.newHttpClient();
// HttpRequest request = HttpRequest.newBuilder()
//     .uri(URI.create("/api/data"))
//     .build();

/* JavaScript Trade-offs:
 * ⚠ Weak typing can cause runtime errors
 * ⚠ Callback hell (mitigated by Promises/async-await)
 * ⚠ Browser inconsistencies (less of an issue now)
 * ⚠ No built-in type safety (TypeScript solves this)
 */

ECMAScript Standard

JavaScript is standardized as ECMAScript, with yearly updates bringing new features.

ECMAScript Evolution
// ES5 (2009) - Baseline modern JS
var array = [1, 2, 3];
var doubled = array.map(function(n) { return n * 2; });

// ES6/ES2015 - Major update
const array = [1, 2, 3];
const doubled = array.map(n => n * 2);

let name = 'Alice';
const greeting = `Hello, ${name}!`; // Template literals

const [first, ...rest] = array;     // Destructuring
const person = { name, age: 30 };   // Shorthand properties

class Person {
    constructor(name) { this.name = name; }
}

// ES2016 (ES7)
const power = 2 ** 3;                // Exponentiation: 8
[1, 2, 3].includes(2);               // true

// ES2017 (ES8)
async function fetchData() {
    const response = await fetch('/api');
    return await response.json();
}

Object.values({ a: 1, b: 2 });       // [1, 2]
Object.entries({ a: 1, b: 2 });      // [['a', 1], ['b', 2]]

// ES2018
const { x, ...others } = { x: 1, y: 2, z: 3 }; // Rest in objects

// ES2019
[1, [2, [3]]].flat(2);               // [1, 2, 3]
array.flatMap(x => [x, x * 2]);      // [1, 2, 2, 4, 3, 6]

// ES2020
const value = null ?? 'default';     // Nullish coalescing
user?.address?.street;               // Optional chaining
Promise.allSettled([p1, p2]);        // All promises settle

// ES2021
const str = 'Hello_World';
str.replaceAll('_', ' ');            // "Hello World"

// ES2022
class Counter {
    #count = 0;  // Private field
    increment() { this.#count++; }
}

await import('./module.js');         // Top-level await

// ES2023
const arr = [1, 2, 3];
arr.toSorted();                      // Non-mutating sort
arr.toReversed();                    // Non-mutating reverse

JavaScript Use Cases

From simple scripts to complex applications, JavaScript powers diverse projects.

Real-World Applications
// 1. Web Applications
// - Single Page Apps (React, Vue, Angular)
// - Progressive Web Apps
// - E-commerce platforms
// - Social networks

// 2. Backend Development
// - REST APIs (Express.js)
// - GraphQL servers (Apollo)
// - Real-time apps (Socket.io)
// - Microservices

// 3. Mobile Development
// - React Native (iOS/Android)
// - Ionic (hybrid apps)
// - NativeScript

// 4. Desktop Applications
// - VS Code (Electron)
// - Slack (Electron)
// - Discord (Electron)

// 5. Game Development
// - Browser games (Phaser, Three.js)
// - 2D/3D graphics
// - WebGL applications

// 6. Machine Learning
// - TensorFlow.js
// - Brain.js
// - ML5.js

// 7. IoT & Robotics
// - Arduino (Johnny-Five)
// - Raspberry Pi
// - Drones

// 8. Automation & Tooling
// - Build tools (Webpack, Vite)
// - Task runners (Gulp)
// - Testing frameworks (Jest, Cypress)
// - Code generators

Practice Exercises

  1. Console Exploration: Open browser DevTools (F12), go to Console tab, and experiment with basic JavaScript expressions.
  2. First Script: Create an HTML file with a button that displays an alert when clicked.
  3. DOM Interaction: Change the text content and style of an HTML element using JavaScript.
  4. Research: Explore the npm registry (npmjs.com) and identify 5 popular JavaScript packages.
  5. Environment Check: Investigate what JavaScript engine your browser uses (V8, SpiderMonkey, JavaScriptCore).
Key Takeaways:
  • JavaScript is a versatile, interpreted language that powers modern web development
  • It runs in browsers, servers (Node.js), mobile apps, desktop apps, and more
  • JavaScript is dynamically typed, event-driven, and asynchronous by nature
  • ECMAScript is the standard specification, with yearly updates adding new features
  • Massive ecosystem with npm providing over 2 million packages
  • Used for front-end, back-end, mobile, desktop, IoT, and game development

What's Next?

Continue with JavaScript History to learn about the language's evolution, or jump to Getting Started to write your first JavaScript code.

JavaScript History

From a 10-day prototype to the world's most popular programming language

JavaScript was created in just 10 days in May 1995 by Brendan Eich at Netscape. What started as a simple scripting language has evolved into a powerful, ubiquitous platform powering billions of devices worldwide.

The Birth of JavaScript (1995)

JavaScript's creation story is one of the most remarkable in computing history.

The Beginning
/* May 1995 - Brendan Eich creates JavaScript in 10 days
 * Original names: Mocha → LiveScript → JavaScript
 *
 * Goals:
 * - Make the web dynamic and interactive
 * - Easy for non-programmers (like Java applets)
 * - Complement Java (marketing decision)
 * - Scheme-like language with C-like syntax
 */

// Early JavaScript (1995)
// Simple form validation
function validateForm() {
    var name = document.forms[0].name.value;
    if (name == "") {
        alert("Please enter your name");
        return false;
    }
    return true;
}

// December 1995: Netscape Navigator 2.0 ships with JavaScript
// Microsoft responds with JScript in Internet Explorer 3.0 (1996)

Browser Wars & Standardization (1996-1999)

The first browser war led to fragmentation and the need for standards.

ECMAScript Emerges
/* Browser Wars Era:
 * 1996: Microsoft releases JScript (IE 3.0)
 * 1996: Netscape submits JavaScript to ECMA for standardization
 * 1997: ECMAScript 1 (ES1) - First standard
 * 1998: ECMAScript 2 (ES2) - Editorial changes
 * 1999: ECMAScript 3 (ES3) - Regular expressions, try/catch
 */

// ES3 (1999) - Modern JavaScript begins
// Regular expressions
var pattern = /[a-z]+/gi;
var text = "Hello World";
var matches = text.match(pattern);

// Try/catch error handling
try {
    riskyOperation();
} catch (error) {
    console.log("Error: " + error.message);
}

// Array methods
var numbers = [1, 2, 3, 4, 5];
var doubled = [];
for (var i = 0; i < numbers.length; i++) {
    doubled.push(numbers[i] * 2);
}

/* The Dark Age:
 * ES4 proposed (2000-2008) - Too ambitious, abandoned
 * Browser inconsistencies plague developers
 * Libraries emerge to paper over differences (Prototype, jQuery)
 */

The Renaissance (2005-2009)

AJAX and new browser engines revitalize JavaScript.

JavaScript Renaissance
/* Key Milestones:
 * 2005: Jesse James Garrett coins "AJAX"
 * 2006: jQuery released - "Write less, do more"
 * 2008: Google Chrome + V8 engine (blazing fast)
 * 2009: Node.js released - JavaScript on the server
 * 2009: ECMAScript 5 (ES5) - JSON, strict mode, getters/setters
 */

// AJAX revolution (2005)
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        var data = JSON.parse(xhr.responseText);
        updateUI(data);
    }
};
xhr.send();

// ES5 (2009) features
"use strict"; // Strict mode

var person = {
    firstName: "John",
    lastName: "Doe",

    // Getter
    get fullName() {
        return this.firstName + " " + this.lastName;
    },

    // Setter
    set fullName(name) {
        var parts = name.split(" ");
        this.firstName = parts[0];
        this.lastName = parts[1];
    }
};

// Array methods
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map(function(n) { return n * 2; });
var evens = numbers.filter(function(n) { return n % 2 === 0; });

// JSON native support
var json = JSON.stringify({ name: "Alice", age: 30 });
var obj = JSON.parse(json);

The Modern Era (2015-Present)

ES6/ES2015 transforms JavaScript into a truly modern language.

ES6+ Revolution
/* ES6/ES2015 - Biggest update ever:
 * - Arrow functions
 * - Classes
 * - Modules
 * - Template literals
 * - Destructuring
 * - Promises
 * - let/const
 * - Spread/rest operators
 * - Default parameters
 */

// Arrow functions
const doubled = numbers.map(n => n * 2);

// Template literals
const name = "Alice";
const greeting = `Hello, ${name}!`;

// Destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
const { name: userName, age } = user;

// Classes
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    greet() {
        return `Hello, I'm ${this.name}`;
    }
}

// Promises
fetch('/api/data')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error(error));

// ES2017 - Async/await
async function fetchData() {
    try {
        const response = await fetch('/api/data');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error(error);
    }
}

// ES2020 - Optional chaining & nullish coalescing
const street = user?.address?.street;
const port = config.port ?? 3000;

// ES2022 - Private fields
class Counter {
    #count = 0;  // Private

    increment() {
        this.#count++;
    }

    getCount() {
        return this.#count;
    }
}

Major Milestones Timeline

Key events that shaped JavaScript's evolution.

Timeline of Innovation
/* JavaScript Timeline:
 *
 * 1995: Brendan Eich creates JavaScript in 10 days
 * 1996: Microsoft releases JScript in IE 3.0
 * 1997: ECMAScript 1 standardized
 * 1999: ES3 - try/catch, regex, Array methods
 * 2005: AJAX coined - Web 2.0 begins
 * 2006: jQuery released - DOM manipulation simplified
 * 2008: Google Chrome + V8 engine launched
 * 2009: Node.js - JavaScript on the server
 * 2009: ES5 - JSON, strict mode, getters/setters
 * 2010: AngularJS released by Google
 * 2013: React released by Facebook
 * 2014: Vue.js released
 * 2015: ES6/ES2015 - Modern JavaScript begins
 * 2016: Yarn package manager
 * 2017: ES2017 - async/await
 * 2018: npm reaches 1 million packages
 * 2019: Optional chaining proposal advances
 * 2020: ES2020 - Optional chaining, nullish coalescing
 * 2021: Rome toolchain announced
 * 2022: ES2022 - Private fields, top-level await
 * 2023: ES2023 - Array methods (toSorted, toReversed)
 * 2024+: Yearly updates continue
 */

// Evolution of writing the same code:

// 1999 (ES3)
function getUsers() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/users', false); // Synchronous!
    xhr.send();
    return JSON.parse(xhr.responseText);
}

// 2009 (ES5)
function getUsers(callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/users', true);
    xhr.onload = function() {
        callback(JSON.parse(xhr.responseText));
    };
    xhr.send();
}

// 2015 (ES6)
function getUsers() {
    return fetch('/api/users')
        .then(response => response.json());
}

// 2017 (ES2017)
async function getUsers() {
    const response = await fetch('/api/users');
    return await response.json();
}

// 2020 (ES2020)
async function getUser(id) {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user?.profile?.name ?? 'Anonymous';
}

Ecosystem Growth

JavaScript's ecosystem has exploded beyond the language itself.

The JavaScript Ecosystem
/* Key Ecosystem Components:
 *
 * Package Managers:
 * - npm (2010) - 2M+ packages
 * - Yarn (2016) - Faster, deterministic
 * - pnpm (2017) - Disk space efficient
 *
 * Build Tools:
 * - Webpack (2012) - Module bundler
 * - Parcel (2017) - Zero config
 * - Vite (2020) - Lightning fast
 * - Turbopack (2022) - Rust-based
 *
 * Frameworks:
 * - jQuery (2006) - DOM manipulation
 * - AngularJS (2010) - Full framework
 * - React (2013) - Component UI
 * - Vue (2014) - Progressive framework
 * - Svelte (2016) - Compiled framework
 * - Solid (2021) - Reactive primitives
 *
 * Server Runtimes:
 * - Node.js (2009) - V8-based
 * - Deno (2020) - Secure by default
 * - Bun (2022) - All-in-one toolkit
 *
 * TypeScript (2012):
 * - Static typing for JavaScript
 * - Catches errors at compile time
 * - Powers VS Code, Angular
 */

// TypeScript example
interface User {
    id: number;
    name: string;
    email: string;
}

async function getUser(id: number): Promise {
    const response = await fetch(`/api/users/${id}`);
    return await response.json();
}

Practice Exercises

  1. Research: Read about Brendan Eich's 10-day creation of JavaScript and the compromises made.
  2. Timeline: Create a visual timeline of JavaScript's major releases and their features.
  3. Code Evolution: Take an ES5 code sample and refactor it using ES6+ features.
  4. Browser Compatibility: Use caniuse.com to check ES6 feature support across browsers.
  5. Ecosystem Exploration: Explore npm trends and identify the most downloaded packages.
Key Takeaways:
  • JavaScript was created in 10 days in 1995 by Brendan Eich at Netscape
  • ES3 (1999) established the foundation with try/catch and regex support
  • AJAX (2005) and V8 engine (2008) sparked the JavaScript renaissance
  • ES6/ES2015 was the biggest update, modernizing the entire language
  • Yearly ECMAScript updates since 2015 continuously add new features
  • Node.js (2009) brought JavaScript to server-side development
  • npm ecosystem grew to over 2 million packages, the largest in existence
  • TypeScript (2012) added optional static typing to JavaScript

What's Next?

Now that you understand JavaScript's history, move on to Getting Started to write your first JavaScript code, or explore Variables & Constants to learn modern syntax.

JavaScript Syntax and Structure

Learn how statements, expressions, and blocks fit together to make readable, maintainable code.

JavaScript syntax is flexible but opinionated. Understanding how statements end, how blocks group logic, and how automatic semicolon insertion (ASI) works helps you avoid subtle bugs. Layer clear identifiers, comments, and strict mode for safer programs.

Statements vs Expressions

Statements perform actions; expressions produce values. Knowing the difference clarifies where each is valid.

Common Statements
let total;                // declaration statement
const user = getUser();  // assignment statement
if (user.active) {       // if statement
  logIn(user);
}
for (let i = 0; i < 3; i++) { // loop statement
  console.log(i);
}
Expressions Everywhere
const price = 25 * 1.08;         // arithmetic expression
const label = `Total: $${price}`; // template literal expression
const active = user && user.online; // logical expression
sendEmail(active ? user.email : 'support@example.com'); // ternary expression

// Expressions can appear inside larger expressions
const score = (hits / totalShots) * 100;

Blocks and Scope

Curly braces create blocks that define scope for let and const. Keep blocks tight and purposeful.

Block Basics
function showStatus(status) {
  if (!status) {
    return 'Unknown';
  }

  {
    // inner block for temporary variables
    const label = status.toUpperCase();
    console.log('Logging label', label);
  }

  return `Status: ${status}`;
}

console.log(showStatus('online'));
Loop Block Scope
const callbacks = [];

for (let i = 0; i < 3; i++) {
  callbacks.push(() => console.log('i is', i));
}

callbacks.forEach(fn => fn()); // 0, 1, 2 thanks to block-scoped let

Semicolons and ASI

JavaScript inserts semicolons automatically in many places, but not all. Write consistent semicolons to avoid edge cases.

Safe Semicolon Usage
const items = [];
items.push('a');
items.push('b');
console.log(items);

// Avoid leading parentheses on new lines after return
function risky() {
  return // ASI inserts semicolon here
  {
    value: 1
  };
}

console.log(risky()); // undefined
ASI Gotchas
const nums = [1, 2, 3];

// Works
[1, 2, 3].forEach(n => console.log(n));

// Breaks if written after a return without semicolon
function broken() {
  return
  [1, 2, 3].map(n => n * 2); // ASI inserts semicolon before array
}

console.log(broken()); // undefined

Comments and Documentation

Use comments to explain why, not what. Prefer short, actionable notes and remove stale comments quickly.

Inline and Block Comments
// TODO: add retry with backoff when network fails
const MAX_RETRIES = 3;

/*
 * Use a block comment for multi-line explanations or API contracts.
 * Keep it concise and keep code self-documenting.
 */
function fetchUser(id) {
  return api.get(`/users/${id}`);
}
JSDoc for Functions
/**
 * Calculate an average, ignoring nullish values.
 * @param  {number[]} values
 * @returns  {number}
 */
function average(values) {
  const valid = values.filter(v => v ?? false);
  return valid.reduce((sum, v) => sum + v, 0) / valid.length;
}

Identifiers and Keywords

Identifiers name variables and functions. Avoid reserved keywords and choose descriptive, consistent names.

Good Naming
const userCount = 42;
let isConnected = true;
function loadProfile(userId) {
  return api.fetch(`/users/${userId}`);
}

// Avoid single-letter names except for small scopes (i, j in loops)
Reserved Keywords
// Examples of reserved words: class, import, export, return, if, while
// Do not use them as identifiers
// const class = 'nope'; // SyntaxError

const ClassName = 'ok';
const importedValue = 10;

Strict Mode

'use strict' opts into a safer subset of JavaScript: it catches silent errors, disallows implicit globals, and reserves future keywords.

Enabling Strict Mode
'use strict';

function assign() {
  // next line would throw ReferenceError instead of creating a global
  // oops = 123;
  return true;
}

assign();
Module Strictness
// ES modules are strict by default
export function greet(name) {
  return `Hello, ${name}`;
}

// CommonJS: enable explicitly
function legacy() {
  'use strict';
  return this === undefined;
}

console.log(legacy()); // true

Organizing Code

Group related logic into modules and functions. Keep files focused and expose a clear API.

Module Layout
// math/utils.js
export function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

export function average(values) {
  return values.reduce((sum, n) => sum + n, 0) / values.length;
}
Importing Modules
// app.js
import { clamp, average } from './math/utils.js';

const temperatures = [68, 72, 70, 75];
console.log('Average', average(temperatures));
console.log('Clamped', clamp(120, 60, 100));

Style and Consistency

Consistent formatting reduces mental overhead. Pick a style (prettier, eslint configs) and stick to it across files.

Common Style Rules
// Prefer const for values that never reassign
// Use single quotes or double quotes consistently
// Indent blocks with 2 spaces or tabs consistently
// Place imports at the top, exports at the bottom

const URL_BASE = 'https://api.example.com';
function getUrl(path) {
  return `${URL_BASE}${path}`;
}
Linting for Syntax Safety
// .eslintrc.js snippet
module.exports = {
  env: { browser: true, es2021: true },
  extends: ['eslint:recommended'],
  rules: {
    'no-unused-vars': 'warn',
    'semi': ['error', 'always'],
    'quotes': ['error', 'single']
  }
};

Practice Exercises

  1. Rewrite three expressions as statements and three statements as expressions; note where each is valid.
  2. Demonstrate an ASI pitfall with return and fix it with explicit semicolons.
  3. Create a small module with two exported functions and import it into another file.
  4. Use strict mode to catch an accidental implicit global and document the error message.
  5. Write an example that shows how let block scope differs from var in a loop.
  6. Add meaningful comments to a function explaining why a decision was made, then remove any redundant comments.
  7. List five reserved keywords and show a valid identifier alternative for each.
  8. Configure a simple ESLint rule set and run it on a file to see reported syntax/style issues.
  9. Format a multi-line expression with parentheses to make operator precedence explicit.
  10. Organize a file by grouping imports, constants, functions, and exports in a consistent order.
Key Takeaways:
  • Distinguish statements from expressions to place code in the right contexts.
  • Use blocks to manage scope, and rely on let/const rather than var.
  • Write explicit semicolons to avoid ASI edge cases, especially after return and before arrays.
  • Adopt strict mode, clear identifiers, and concise comments for safer, more readable code.
  • Organize files into modules and enforce style with automated tooling.

What's Next?

Continue to Control Flow to see how syntax powers branching and decisions, or jump to Loops and Iteration for repeated execution patterns.

Variables and Constants

Master modern declarations with let and const, understand scoping, and avoid subtle hoisting pitfalls.

Bindings are the foundation of every program. Knowing when to choose let, when to lock values with const, and when to avoid var will keep your code predictable and maintainable.

Choosing Between let and const

Default to const for values that should not be reassigned. Use let when you truly need reassignment inside a block.

Practical Declarations
const apiUrl = '/v1';      // stable reference
let retryCount = 0;        // will change

retryCount += 1;           // ok
// apiUrl = '/v2';         // TypeError: Assignment to constant variable

Block Scope Basics

Variables declared with let and const live inside their nearest block ({ ... }). This prevents accidental leaks into surrounding code.

Block-Scoped Counters
for (let i = 0; i < 3; i++) {
  const label = `step-${i}`;
  console.log(label);
}

// console.log(i);       // ReferenceError: i is not defined
// console.log(label);   // ReferenceError: label is not defined

Understanding Hoisting

var declarations are hoisted and initialized to undefined, which can hide bugs. let and const are hoisted too but stay in the temporal dead zone until the declaration runs.

Hoisting Contrast
console.log(total); // undefined (var is hoisted)
var total = 5;

// console.log(count); // ReferenceError (TDZ for let)
let count = 5;

Temporal Dead Zone (TDZ)

The TDZ prevents access to let and const bindings before their declaration. This enforces clear ordering and reduces runtime surprises.

Safe Initialization
function createUser(name) {
  // Accessing user before declaration would throw
  const user = { name, createdAt: new Date() };
  return user;
}

// const output = user; // ReferenceError if uncommented before declaration
const output = createUser('Dev');
console.log(output);

Naming Conventions and Intent

Use clear, intention-revealing names. Prefer nouns for data (userProfile), verbs for actions (calculateTotal), and uppercase constants for configuration values you never expect to change at runtime.

Readable Names
const MAX_RETRIES = 3;
let currentRetry = 0;

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

const total = calculateTotal([
  { name: 'Book', price: 12 },
  { name: 'Pen', price: 2 }
]);
console.log(total);

const with Objects and Arrays

const prevents reassignment of the binding, not mutation of the value. You can still change properties or array items.

Mutating const Collections
const user = { name: 'Kai', role: 'author' };
user.role = 'editor';      // allowed
// user = {};             // TypeError: Assignment to constant variable

const tags = ['js', 'web'];
tags.push('es6');          // allowed
console.log(tags.join(', '));

When var Still Appears

Legacy code may still use var. Understand its function scope and hoisting so you can refactor safely.

Refactoring var to let/const
function legacyFlag(condition) {
  if (condition) {
    var flag = true; // function-scoped, leaks outside the block
  }
  return flag; // returns true or undefined
}

function saferFlag(condition) {
  if (!condition) return false;
  const flag = true; // block-scoped
  return flag;
}

Patterns for Safe Reassignment

Reassign only when it communicates state change, such as counters, accumulators, or temporary swaps. Otherwise, favor immutable patterns.

Stateful Loop vs Immutable Map
// Reassign with let when accumulating
let totalMinutes = 0;
for (const session of [30, 45, 25]) {
  totalMinutes += session;
}

// Prefer const when using map/filter/reduce
const durations = [30, 45, 25];
const doubled = durations.map(minutes => minutes * 2);
console.log({ totalMinutes, doubled });

Destructuring with let and const

Use destructuring to pull fields from objects and arrays while keeping scoping clear. Default to const unless mutation is required.

Destructuring Examples
const userProfile = {
  name: 'Riley',
  email: 'riley@example.com',
  plan: 'pro'
};

const { name, plan } = userProfile;
console.log(name, plan);

let [first, second] = ['alpha', 'beta'];
[first, second] = [second, first]; // swap with reassignment
console.log(first, second);

Shadowing and Visibility

Shadowing happens when an inner scope reuses a name from an outer scope. Keep it intentional and short-lived to avoid confusion.

Scope Shadowing
const status = 'global';

function showStatus() {
    const status = 'function'; // shadows outer
    if (true) {
        let status = 'block';    // shadows function-level
        console.log(status);     // block
    }
    console.log(status);       // function
}

showStatus();
console.log(status);         // global

Freezing Configuration

Use Object.freeze to prevent accidental mutations to critical configuration objects while still benefiting from const bindings.

Object.freeze in Practice
const CONFIG = Object.freeze({
    apiBase: 'https://api.example.com',
    timeoutMs: 5000
});

// CONFIG.apiBase = 'https://malicious.site'; // no effect in strict mode, throws in strict
console.log(CONFIG.apiBase);

// Note: freeze is shallow; nest carefully
const SAFE_CONFIG = Object.freeze({
    headers: Object.freeze({ Accept: 'application/json' })
});

console.log(SAFE_CONFIG.headers.Accept);

const Inside Loops

const can appear in loops because each iteration creates a new binding. This keeps values stable within that single pass while avoiding accidental reuse.

Loop Bindings Per Iteration
const ids = [101, 102, 103];

for (const id of ids) {
    // id is a new binding on every iteration
    const message = `Processing ${id}`;
    console.log(message);
}

// Using let for counters when you mutate the value
for (let index = 0; index < ids.length; index++) {
    console.log('Index', index, 'Id', ids[index]);
}

Practice Exercises

  1. Refactor Legacy: Take a snippet that uses var and convert it to let/const, confirming behavior stays the same.
  2. Scope Guard: Create a loop that declares a block-scoped variable and verify it is not accessible outside the block.
  3. Immutable Config: Define a configuration object with const and update one of its properties safely.
  4. TDZ Experiment: Intentionally access a let binding before its declaration to observe the ReferenceError, then fix the order.
  5. Swap Values: Use array destructuring with let to swap two variables without a temporary placeholder.
  6. Naming Audit: Rename ambiguous variables in a small function to communicate intent clearly.
Key Takeaways:
  • Prefer const by default; reach for let only when reassignment is required.
  • let and const are block-scoped, preventing accidental leaks.
  • var is function-scoped and hoisted; refactor it out in modern code.
  • The temporal dead zone enforces ordering and reduces undefined access bugs.
  • const protects the binding, not the contents of objects or arrays.
  • Readable, intention-revealing names make state changes obvious.
  • Destructuring works cleanly with const and let for both objects and arrays.
  • Use reassignment as a deliberate signal of state change, not a default habit.

What's Next?

Move on to data types and operations to learn how these bindings interact with strings, numbers, and objects across your applications.

JavaScript Data Types

Understand primitives, objects, and how JavaScript checks and coerces values in real applications.

JavaScript values come in two broad families: primitives and objects. Knowing how they behave, how to detect them, and how coercion works will prevent subtle bugs in APIs, forms, and calculations.

Strings and Template Literals

Strings represent text and support interpolation with template literals, making dynamic messages concise.

String Basics
const name = 'Nova';
const greeting = `Hello, ${name}!`;
console.log(greeting);

const multiline = `Line one
Line two
Line three`;
console.log(multiline);

Numbers, NaN, and Infinity

Numbers cover integers and floats. Be mindful of division by zero, invalid math, and floating-point quirks.

Number Gotchas
const price = 9.99;
const quantity = 3;
const total = price * quantity; // 29.97

console.log(1 / 0);           // Infinity
console.log(Math.sqrt(-1));   // NaN
console.log(Number.isNaN(NaN)); // true

// Floating point precision
console.log(0.1 + 0.2);       // 0.30000000000000004
console.log(Number((0.1 + 0.2).toFixed(2))); // 0.3

Booleans and Truthiness

Booleans are strictly true or false, but many values implicitly convert in conditions. Understand truthy/falsy to write predictable checks.

Truthy and Falsy Values
const inputs = ['text', '', 0, 42, null, undefined, [], {}];

inputs.forEach(value => {
  if (value) {
    console.log(value, 'is truthy');
  } else {
    console.log(value, 'is falsy');
  }
});

null vs undefined

null is an intentional empty value you set. undefined usually means “not provided” or “not initialized.”

Intentional vs Missing
let user;
console.log(user); // undefined

const profile = {
  name: 'Avi',
  bio: null // intentionally empty until user writes one
};

console.log(profile.bio === null);       // true
console.log('age' in profile);           // false
console.log(profile.missingProp);        // undefined

Symbols and Uniqueness

Symbols create unique keys that avoid collisions, useful for metadata or library internals.

Symbol Keys
const id = Symbol('id');
const user = { name: 'Ivy', [id]: 12345 };

console.log(user[id]);         // 12345
console.log(Object.keys(user)); // ['name'] — symbol is hidden from keys

BigInt for Large Integers

Use BigInt when numbers exceed safe integer limits. Add n to the end of literals or call BigInt().

Working with BigInt
const maxSafe = Number.MAX_SAFE_INTEGER; // 9007199254740991
const bigger = BigInt(maxSafe) + 10n;

console.log(maxSafe + 10); // still 9007199254741000 (precision risk)
console.log(bigger);       // 9007199254741001n

Objects and Arrays

Objects store keyed collections; arrays store ordered lists. Both are reference types, so assignments copy references, not values.

Reference Behavior
const original = { city: 'Lagos' };
const alias = original;     // same reference
alias.city = 'Nairobi';

console.log(original.city); // Nairobi

const list = ['alpha', 'beta'];
const copy = [...list];     // shallow copy with spread
copy.push('gamma');
console.log(list, copy);

Type Checking with typeof and Helpers

typeof works for primitives but returns "object" for arrays and null. Combine it with helpers like Array.isArray.

Reliable Checks
console.log(typeof 'hi');          // string
console.log(typeof 42);            // number
console.log(typeof null);          // object (historical quirk)
console.log(Array.isArray([]));    // true
console.log(typeof Symbol('x'));   // symbol
console.log(typeof 10n);           // bigint

function isPlainObject(value) {
  return Object.prototype.toString.call(value) === '[object Object]';
}

console.log(isPlainObject({}));    // true
console.log(isPlainObject(new Date())); // false

Type Coercion Rules

JavaScript will coerce types in arithmetic and comparisons. Prefer strict equality and explicit casts to avoid surprises.

Coercion Examples
console.log('5' + 1);        // "51" (string concatenation)
console.log('5' - 1);        // 4 (string coerced to number)
console.log(Boolean(''));    // false
console.log(Boolean('hi'));  // true

console.log(0 == false);     // true (coercion)
console.log(0 === false);    // false (no coercion)

const amount = Number('42'); // explicit conversion
console.log(amount + 8);     // 50

Copying and Comparing Values

Primitives copy by value; objects copy by reference. Deep comparison requires custom logic or utility libraries.

Shallow vs Deep
const a = 5;
const b = a; // value copy

const obj1 = { theme: 'dark', lang: 'en' };
const obj2 = { ...obj1 }; // shallow copy

console.log(a === b);      // true
console.log(obj1 === obj2); // false (different references)

// Simple deep clone for JSON-safe objects
const clone = JSON.parse(JSON.stringify(obj1));
console.log(clone);

Dates, JSON, and Serialization

Dates are objects, not primitives. JSON serialization turns objects into strings; numbers and strings survive intact, but functions and symbols are ignored.

Working with Dates
const now = new Date();
console.log(now.toISOString());
console.log(now.getFullYear());

// Avoid storing dates as strings if you need math; keep Date instances or timestamps
const timestamp = now.getTime();
console.log(new Date(timestamp));
JSON.stringify and parse
const payload = {
  id: 7,
  active: true,
  createdAt: new Date().toISOString(),
  tags: ['js', 'learning']
};

const json = JSON.stringify(payload); // object -> string
console.log(json);

const parsed = JSON.parse(json);     // string -> object
console.log(parsed.tags[0]);

// Functions and symbols drop during serialization
console.log(JSON.stringify({ fn: () => 'hi', sym: Symbol('x') })); // {}

Practice Exercises

  1. Type Audit: Log the typeof result for every field in an object that mixes strings, numbers, booleans, and arrays.
  2. Coercion Catch: Write comparisons using == and rewrite them with ===, noting behavioral changes.
  3. BigInt Limit: Find a number larger than Number.MAX_SAFE_INTEGER and represent it safely with BigInt.
  4. Clone and Mutate: Clone an object, mutate the clone, and verify the original is unchanged.
  5. Null Handling: Build a function that returns null when data is intentionally empty and undefined when a property is missing.
  6. Precision Fix: Create a helper that rounds currency totals to two decimals to avoid floating-point artifacts.
Key Takeaways:
  • JavaScript primitives are string, number, boolean, null, undefined, symbol, and bigint.
  • Objects and arrays are reference types; assignments copy references, not values.
  • typeof works for primitives but needs helpers for arrays and null.
  • Watch out for NaN, Infinity, and floating-point precision issues in calculations.
  • Use strict equality and explicit casts to avoid unwanted coercion.
  • null signals intentional emptiness; undefined means missing or uninitialized.
  • BigInt handles integers beyond safe numeric limits.
  • Copy primitives by value and objects by reference; clone when you need isolation.

What's Next?

Proceed to expressions and operators to see how these data types combine in calculations, conditions, and real application flows.

Operators and Expressions

Combine values with operators to compute results, compare data, and control program flow.

Expressions produce values; operators transform or combine those values. Mastering the operator toolkit helps you read and write concise, intentional code across arithmetic, comparison, logical reasoning, and newer capabilities like optional chaining and nullish coalescing.

Arithmetic Operators

Use arithmetic to compute totals, percentages, and formatted strings. Watch out for integer vs floating-point quirks.

Core Arithmetic
const subtotal = 49.99 * 3;
const taxRate = 0.0825;
const tax = +(subtotal * taxRate).toFixed(2); // force numeric with unary +
const total = subtotal + tax;
const perItem = total / 3;
const remainder = 17 % 5;      // 2
const exponent = 2 ** 5;       // 32
const discount = total - 5;    // subtract a coupon

console.log({ subtotal, tax, total, perItem, remainder, exponent, discount });
Increment and Decrement
let stock = 10;
console.log(stock++); // 10 (post-increment returns old value)
console.log(stock);   // 11

let seats = 5;
console.log(++seats); // 6 (pre-increment returns new value)
console.log(seats);   // 6

let countdown = 3;
while (countdown--) {
  console.log('Launching in', countdown);
}

Assignment and Compound Operators

Compound assignments shorten updates and clarify intent, especially when mutating counters or accumulating totals.

Compound Updates
let balance = 1000;
balance += 250; // deposit
balance -= 90;  // purchase
balance *= 1.05; // interest
balance /= 2;   // split between accounts

let flags = 0b0011;
flags |= 0b1000; // set bit 4
flags &= 0b0101; // mask bits
Destructuring Assignments
const user = { name: 'Sam', role: 'admin', active: true };
const { name, role, active = false } = user;

const rgb = [255, 128, 64];
const [red, green, blue, alpha = 1] = rgb;

console.log(name, role, active);
console.log(red, green, blue, alpha);

Comparison Operators

Strict comparisons avoid implicit type coercion. Use === and !== for predictable results.

Equality vs Strict Equality
console.log(2 == '2');   // true (coerced)
console.log(2 === '2');  // false (strict)
console.log(null == undefined);  // true
console.log(null === undefined); // false

const age = 18;
const canVote = age >= 18;
const isTeen = age >= 13 && age <= 19;
console.log({ canVote, isTeen });
Lexicographical Comparisons
console.log('apple' < 'banana');      // true
console.log('Z' > 'a');               // false (uppercase sorts before lowercase)

const words = ['delta', 'alpha', 'charlie'];
const sorted = [...words].sort();
console.log(sorted); // ['alpha', 'charlie', 'delta']

Logical, Nullish, and Optional Chaining

Logical operators short-circuit, making them perfect for fallbacks and guards. Nullish coalescing and optional chaining reduce noisy checks.

AND/OR Short-Circuit
const cached = null;
const fromApi = () => 'live-data';

const result = cached || fromApi(); // OR returns first truthy
const mustBeReady = true && 'go';   // AND returns last value if all truthy

console.log({ result, mustBeReady });

const user = { settings: { theme: 'dark' } };
const theme = user && user.settings && user.settings.theme;
console.log(theme); // 'dark'
Nullish and Optional Chaining
const config = { api: { retries: 0, timeout: 5000 } };
const retries = config.api?.retries ?? 3;    // 0 is respected
const timeout = config.api?.timeout ?? 3000; // 5000
const locale = config.user?.preferences?.locale ?? 'en-US';

console.log({ retries, timeout, locale });

const response = { data: null };
const message = response.data?.title ?? 'No data yet';
console.log(message);

Ternary Expressions and Guards

The ternary operator packs concise conditional expressions. Pair with guard clauses for readable branches.

Ternary Patterns
const score = 82;
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : 'C';
const label = score >= 70 ? 'Pass' : 'Retake';

const status = isLoggedIn
  ? `Welcome back, ${user.name}`
  : 'Please sign in';

console.log({ grade, label, status });
Guard Clauses
function sendEmail(user) {
  if (!user?.email) return 'Missing email';
  if (!user.verified) return 'Verify account first';

  // main path
  return `Sent to ${user.email}`;
}

console.log(sendEmail({ email: 'a@example.com', verified: true }));
console.log(sendEmail({ email: null, verified: true }));

Bitwise Operators

Bitwise operators toggle flags efficiently. They work on 32-bit integers and are common in permissions and feature toggles.

Flag Management
const CAN_READ = 1 << 0;   // 0001
const CAN_WRITE = 1 << 1;  // 0010
const CAN_DELETE = 1 << 2; // 0100

let permissions = 0;
permissions |= CAN_READ;
permissions |= CAN_WRITE;

const canDelete = (permissions & CAN_DELETE) !== 0;
const canWrite = (permissions & CAN_WRITE) !== 0;

console.log({ canDelete, canWrite });
Bitwise Tricks
console.log(~5);          // bitwise NOT => -6
console.log(5 & 3);       // 1 (0101 & 0011)
console.log(5 | 3);       // 7 (0101 | 0011)
console.log(5 ^ 3);       // 6 (0101 ^ 0011)

// Truncate to 32-bit integer
console.log(3.9 | 0);     // 3

Spread and Rest

Spread copies iterable values into new arrays or objects. Rest collects the remaining values into an array.

Spread in Arrays and Objects
const original = [1, 2, 3];
const extended = [...original, 4, 5];

const defaults = { theme: 'light', compact: false };
const overrides = { compact: true };
const settings = { ...defaults, ...overrides };

console.log({ extended, settings });
Rest Parameters
function sum(label, ...numbers) {
  const total = numbers.reduce((acc, n) => acc + n, 0);
  return `${label}: ${total}`;
}

const stats = sum('Points', 12, 8, 15, 20);
console.log(stats);

const [first, ...others] = ['alpha', 'beta', 'gamma'];
console.log(first, others);

typeof and instanceof

Check primitive types with typeof and object inheritance with instanceof. Remember arrays are objects.

Type Checks
console.log(typeof 42);          // 'number'
console.log(typeof 'hi');        // 'string'
console.log(typeof null);        // 'object' (historical quirk)
console.log(typeof undefined);   // 'undefined'
console.log(typeof (() => {}));  // 'function'
console.log(Array.isArray([]));  // true
Instance Checks
class Animal {}
class Dog extends Animal {}

const rover = new Dog();
console.log(rover instanceof Dog);    // true
console.log(rover instanceof Animal); // true
console.log(rover instanceof Object); // true

const date = new Date();
console.log(date instanceof Date);    // true
console.log(date instanceof Object);  // true

Expression Evaluation and Precedence

Operator precedence controls evaluation order. Use parentheses to clarify complex expressions and avoid surprises.

Precedence in Action
const result = 4 + 3 * 2;      // 10, multiplication first
const grouped = (4 + 3) * 2;   // 14

const chain = 1 || 0 && false; // 1, AND before OR
const safe = (1 || 0) && false; // false

console.log({ result, grouped, chain, safe });
Expression Composition
const price = 120;
const coupon = 10;
const member = true;

const final = (price - coupon) * (member ? 0.9 : 1);
const label = `Final: $${final.toFixed(2)}`;
console.log(label);

// Compose logical and arithmetic
const isEligible = price > 100 && member;
console.log({ isEligible });

Practice Exercises

  1. Calculate a shopping cart total with tax, discount, and shipping using arithmetic and compound assignments.
  2. Write a guard clause function that returns early if a user is missing an email or has not accepted terms.
  3. Create a permissions bitmask for read/write/delete and check each permission with bitwise operators.
  4. Use optional chaining and nullish coalescing to safely read nested API response data.
  5. Implement a ternary-based grade calculator that outputs A/B/C/F based on numeric ranges.
  6. Build a sum(...numbers) function with rest parameters and test it with different lengths.
  7. Demonstrate typeof checks for primitives and instanceof checks for custom classes.
  8. Refactor a complex logical expression by adding parentheses to make precedence explicit.
  9. Spread an array of scores into a new array and append two extra scores without mutating the original.
  10. Explore short-circuiting by logging which functions run inside a && b() and a || b().
Key Takeaways:
  • Prefer strict comparisons and clear precedence with parentheses for predictable expressions.
  • Short-circuiting, nullish coalescing, and optional chaining reduce verbose safety checks.
  • Spread/rest, destructuring, and compound assignments create concise, immutable-friendly code.
  • Bitwise operators are compact tools for flags; document meaning to keep them readable.
  • typeof targets primitives, while instanceof checks prototype inheritance.

What's Next?

Move on to JavaScript Syntax and Structure to see how operators fit inside statements, or jump to Control Flow to combine expressions with branching.

Control Flow

Guide your program with branches, guards, and structured error handling.

Control flow lets you decide what runs and when. Combine if/else, switch, ternaries, and guard clauses to keep logic readable. Understand truthy/falsy values and handle errors with try/catch/finally to build resilient code.

If/Else Foundations

Use if for simple branching. Chain else if for ranges and keep branches minimal.

Basic Branching
function grade(score) {
  if (score >= 90) {
    return 'A';
  } else if (score >= 80) {
    return 'B';
  } else if (score >= 70) {
    return 'C';
  }
  return 'Needs work';
}

console.log(grade(85));
Nested vs Flat
// Deep nesting hurts readability
function access(user) {
  if (user) {
    if (user.active) {
      if (user.role === 'admin') {
        return 'full access';
      }
    }
  }
  return 'limited access';
}

// Flatten with early returns
function accessFlat(user) {
  if (!user) return 'limited access';
  if (!user.active) return 'limited access';
  if (user.role !== 'admin') return 'limited access';
  return 'full access';
}

Ternary and Guards

Ternaries compress small decisions into expressions. Guard clauses return early to prevent deep nesting.

Ternary Decisions
const points = 120;
const tier = points > 200 ? 'gold' : points > 100 ? 'silver' : 'bronze';
const label = points >= 100 ? 'Qualified' : 'Pending';

console.log({ tier, label });
Guard Clauses
function sendBonus(user) {
  if (!user?.email) return 'No email on file';
  if (!user.active) return 'Inactive user';

  return `Bonus sent to ${user.email}`;
}

console.log(sendBonus({ email: 'a@example.com', active: true }));

Switch for Multi-Way Branching

switch shines when many discrete cases share logic. Always include a default branch.

Switch with Fallthrough
function shippingCost(zone) {
  switch (zone) {
    case 'local':
      return 5;
    case 'regional':
    case 'national':
      return 10; // fallthrough groups cases
    case 'international':
      return 25;
    default:
      return 15;
  }
}

console.log(shippingCost('regional'));
Switch on true for Ranges
function getBadge(score) {
  switch (true) {
    case score >= 90: return 'Platinum';
    case score >= 75: return 'Gold';
    case score >= 60: return 'Silver';
    default: return 'Bronze';
  }
}

console.log(getBadge(78));

Truthy, Falsy, and Nullish

JavaScript treats some values as false in conditionals. Know the list and use nullish checks when zero or empty strings are valid.

Truthy and Falsy Examples
const falsyValues = [false, 0, -0, '', null, undefined, NaN];
falsyValues.forEach(value => {
  if (value) {
    console.log('truthy? nope');
  } else {
    console.log('falsy detected');
  }
});

console.log(Boolean('hello')); // true
console.log(Boolean([]));      // true (arrays are truthy)
Nullish Coalescing
const attempts = 0;        // valid zero
const retries = attempts ?? 3; // keeps 0

const response = { data: null };
const content = response.data ?? 'Loading...';

console.log({ retries, content });

Defensive Checks

Combine optional chaining with nullish coalescing to safely read nested properties without crashes.

Safe Property Access
const session = { user: { profile: { name: 'Ravi' } } };
const name = session.user?.profile?.name ?? 'Guest';
const city = session.user?.profile?.address?.city ?? 'Unknown';

console.log({ name, city });
Guarding Function Calls
const analytics = {
  track(event) {
    console.log('Tracking', event);
  }
};

analytics.track?.('page_view');
const maybeTrack = null;
maybeTrack?.('noop'); // safe no-op

Error Handling with try/catch/finally

Wrap risky code to recover gracefully. Use finally for cleanup that must always run.

Basic try/catch
function parseJson(input) {
  try {
    return JSON.parse(input);
  } catch (error) {
    console.error('Could not parse JSON', error.message);
    return null;
  }
}

console.log(parseJson('{"ok":true}'));
console.log(parseJson('bad')); // null
try/catch/finally
function readConfig(load) {
  let config;
  try {
    config = load();
    if (!config.enabled) throw new Error('Disabled');
    return config;
  } catch (error) {
    console.warn('Fallback to defaults', error.message);
    return { enabled: false };
  } finally {
    console.log('Config attempted');
  }
}

readConfig(() => ({ enabled: true }));
readConfig(() => { throw new Error('Missing file'); });

Early Returns and Guards

Early returns reduce indentation and highlight happy paths. Combine with validation for cleaner control flow.

Validation Flow
function processOrder(order) {
  if (!order) return 'Missing order';
  if (!order.items?.length) return 'Empty cart';
  if (order.total <= 0) return 'Total invalid';

  // happy path
  return `Processing ${order.items.length} items`;
}

console.log(processOrder({ items: ['a', 'b'], total: 50 }));
Combining Conditions
function canPublish(user) {
  const hasRole = user?.role === 'editor' || user?.role === 'admin';
  const isTrusted = Boolean(user?.verified) && !user.suspended;
  return hasRole && isTrusted;
}

console.log(canPublish({ role: 'editor', verified: true, suspended: false }));

Practice Exercises

  1. Rewrite a nested if chain using guard clauses to flatten the structure.
  2. Implement a switch that groups multiple cases together and includes a default.
  3. Create a function that distinguishes between 0, null, and undefined using nullish coalescing.
  4. Show how a ternary can replace a simple if and when it should not.
  5. List all falsy values in JavaScript and test each in an if statement.
  6. Wrap a JSON parsing call in try/catch and log a friendly error message.
  7. Demonstrate finally by releasing a resource (e.g., clearing a timer) regardless of success.
  8. Use optional chaining to safely access a deep property on an object from an API.
  9. Build a permission check that returns early when the user is missing required roles.
  10. Combine logical operators with parentheses to make precedence clear in a complex condition.
Key Takeaways:
  • Flatten control flow with guard clauses and early returns to keep happy paths visible.
  • Use switch for multi-branch decisions and include a default case.
  • Know truthy/falsy values and prefer nullish checks when zero or empty strings are acceptable.
  • Handle errors with try/catch/finally to recover gracefully and clean up.
  • Optional chaining and ternaries make concise, safe decisions when used thoughtfully.

What's Next?

Continue to Loops and Iteration to repeat work efficiently, or review Operators and Expressions to strengthen the building blocks of your conditions.

Loops and Iteration

Repeat work efficiently with classic loops, array iterators, and control statements.

Iteration patterns shape performance and readability. Choose the loop that matches your data and termination conditions. Modern array helpers like map, filter, and reduce encourage declarative thinking, while break, continue, and labels help you escape or skip work precisely.

Classic for Loop

Use the indexed for loop when you need the index, custom step sizes, or early exits.

Counting with for
const logs = [];
for (let i = 0; i < 5; i++) {
  logs.push(`Step ${i}`);
}
console.log(logs);

for (let i = 10; i >= 0; i -= 2) {
  console.log('Countdown', i);
}
Early Exit with break
const numbers = [1, 3, 5, 8, 9];
let firstEven = null;

for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    firstEven = numbers[i];
    break; // stop searching
  }
}

console.log(firstEven); // 8

while and do...while

while loops run while a condition remains true. do...while runs at least once.

while Loop
let attempts = 0;
while (attempts < 3) {
  console.log('Attempt', attempts + 1);
  attempts++;
}
do...while Loop
let password;
do {
  password = prompt('Enter password');
} while (!password);

console.log('Password captured');

for...of for Iterables

for...of walks through iterable values like arrays, strings, Maps, and Sets. It is concise and readable.

Iterating Arrays
const fruits = ['apple', 'banana', 'cherry'];
for (const fruit of fruits) {
  console.log(fruit.toUpperCase());
}

const sentence = 'loop';
for (const char of sentence) {
  console.log(char);
}
Maps and Sets
const map = new Map([
  ['id', 3],
  ['name', 'Asha']
]);
for (const [key, value] of map) {
  console.log(key, value);
}

const set = new Set([1, 2, 2, 3]);
for (const value of set) {
  console.log(value); // 1, 2, 3
}

for...in for Object Keys

for...in iterates enumerable keys. Use it for plain objects, not arrays, to avoid unexpected order.

Iterating Object Properties
const user = { name: 'Lee', role: 'editor', active: true };
for (const key in user) {
  if (Object.hasOwn(user, key)) {
    console.log(key, user[key]);
  }
}
Avoid for...in on Arrays
const arr = ['x', 'y', 'z'];
for (const index in arr) {
  console.log(index); // '0', '1', '2' (strings)
}
// Prefer for...of or array helpers for arrays

Array Helpers: forEach, map

Array methods express iteration declaratively. forEach executes side effects; map transforms.

forEach Side Effects
const emails = [];
['a@x.com', 'b@x.com', 'c@x.com'].forEach((email, index) => {
  emails.push({ id: index + 1, email });
});
console.log(emails);

// Note: break/continue do not work with forEach
map for Transformation
const prices = [5, 10, 20];
const withTax = prices.map(price => ({
  price,
  total: +(price * 1.08).toFixed(2)
}));

console.log(withTax);

filter, find, some, every

Filter arrays to matching elements, locate a single element, or check predicates across the whole collection.

Filtering Collections
const users = [
  { name: 'Ava', active: true },
  { name: 'Bo', active: false },
  { name: 'Cal', active: true }
];

const activeUsers = users.filter(u => u.active);
const firstInactive = users.find(u => !u.active);
const allActive = users.every(u => u.active);
const someoneInactive = users.some(u => !u.active);

console.log({ activeUsers, firstInactive, allActive, someoneInactive });
Combining Filters
const products = [
  { name: 'Pen', price: 1.5, tags: ['office'] },
  { name: 'Notebook', price: 4, tags: ['office', 'paper'] },
  { name: 'Marker', price: 2, tags: ['office'] }
];

const underThree = products.filter(p => p.price < 3);
const paperRelated = products.filter(p => p.tags.includes('paper'));

console.log({ underThree, paperRelated });

reduce for Aggregation

reduce combines array values into a single result: numbers, objects, or even promises.

Summing with reduce
const totals = [5, 10, 15];
const sum = totals.reduce((acc, value) => acc + value, 0);
console.log(sum); // 30
Grouping with reduce
const orders = [
  { status: 'shipped', id: 1 },
  { status: 'pending', id: 2 },
  { status: 'shipped', id: 3 }
];

const grouped = orders.reduce((acc, order) => {
  acc[order.status] = acc[order.status] || [];
  acc[order.status].push(order.id);
  return acc;
}, {});

console.log(grouped);

break, continue, and Labels

Control loop execution manually. Labels help exit nested loops, but use them sparingly for clarity.

Skipping with continue
for (let i = 1; i <= 5; i++) {
  if (i % 2 === 0) continue; // skip even numbers
  console.log('Odd', i);
}
Labeled break
outer: for (let row = 0; row < 3; row++) {
  for (let col = 0; col < 3; col++) {
    if (row === col) {
      console.log('Diagonal found', row, col);
      break outer; // exits both loops
    }
  }
}

Practice Exercises

  1. Write a classic for loop that counts down by 3s and stops at zero.
  2. Use for...of to iterate a string and collect vowel counts.
  3. Iterate an object with for...in while guarding with Object.hasOwn.
  4. Build a list of objects using forEach and note why break will not work.
  5. Create a transformation pipeline with map, filter, and reduce to compute totals.
  6. Demonstrate do...while by prompting until valid input arrives.
  7. Find the first item that matches a predicate using a loop with break and compare to find.
  8. Show how continue skips specific iterations in a validation loop.
  9. Use a labeled break to exit nested loops when a target cell is found.
  10. Measure readability by rewriting a for loop into array helpers and note trade-offs.
Key Takeaways:
  • Pick the loop that matches your data: for for indices, for...of for iterables, for...in for object keys.
  • Array helpers like map, filter, and reduce encourage declarative, side-effect-free transformations.
  • break and continue shape control inside loops; labels should be rare and intentional.
  • while and do...while suit unknown iteration counts or at-least-once flows.
  • Prefer readability and intent over micro-optimizations; comment complex loop logic when necessary.

What's Next?

Head to Functions to package loop logic into reusable pieces, or revisit Operators and Expressions to refine the calculations inside your iterations.

Functions

Define reusable logic with declarations, expressions, and modern parameter patterns.

Functions are the building blocks of JavaScript programs. They encapsulate behavior, accept inputs, and return outputs. Explore declarations, expressions, arrow functions, parameters, defaults, rest, closures, and immediately invoked function expressions (IIFEs) to write expressive, modular code.

Function Declarations and Expressions

Declarations are hoisted; expressions are assigned to variables and can be passed around freely.

Declaration
function greet(name) {
  return `Hello, ${name}`;
}

console.log(greet('Mia'));
Expression
const greetUser = function(name) {
  return `Welcome, ${name}`;
};

console.log(greetUser('Dev'));

Arrow Functions

Arrow functions are concise and lexically bind this. They are great for callbacks and small utilities.

Concise vs Block Body
const double = n => n * 2;
const formatName = (first, last) => `${first} ${last}`;

const describe = (user) => {
  const role = user.role ?? 'guest';
  return `${user.name} (${role})`;
};

console.log(double(4));
console.log(formatName('Ada', 'Lovelace'));
console.log(describe({ name: 'Nia', role: 'admin' }));
Arrow this Binding
const counter = {
  value: 0,
  increment() {
    setInterval(() => {
      this.value++;
      console.log('Value', this.value);
    }, 1000);
  }
};

counter.increment(); // arrow preserves outer this

IIFE (Immediately Invoked Function Expression)

IIFEs run as soon as they are defined. They create a private scope for setup code.

Classic IIFE
const settings = (() => {
  const secret = 'token-123';
  const baseUrl = 'https://api.example.com';
  return { baseUrl, getToken: () => secret };
})();

console.log(settings.getToken());
Async IIFE
(async () => {
  const response = await fetch('https://api.quotable.io/random');
  const data = await response.json();
  console.log('Quote', data.content);
})();

Parameters, Defaults, and Rest

Provide default values, collect variable arguments, and destructure parameters for clarity.

Defaults and Destructuring
function createUser({ name, role = 'viewer', active = true }) {
  return { name, role, active };
}

console.log(createUser({ name: 'Rita' }));
console.log(createUser({ name: 'Vic', role: 'editor', active: false }));
Rest Parameters
function sum(label, ...values) {
  const total = values.reduce((acc, n) => acc + n, 0);
  return `${label}: ${total}`;
}

console.log(sum('Scores', 10, 15, 20));

Return Values and Early Exits

Return values explicitly. Use early returns to handle invalid states quickly.

Clear Returns
function divide(a, b) {
  if (b === 0) return null;
  return a / b;
}

console.log(divide(10, 2));
console.log(divide(10, 0));
Early Guard
function sendNotification(user) {
  if (!user?.email) return 'No email';
  if (!user.optedIn) return 'No consent';
  return `Sent to ${user.email}`;
}

console.log(sendNotification({ email: 'x@y.com', optedIn: true }));

Recursion Basics

Recursion solves problems by calling the function inside itself. Always include a base case to avoid infinite loops.

Factorial
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120
Traversing Trees
const tree = {
  value: 'root',
  children: [
    { value: 'a', children: [] },
    { value: 'b', children: [{ value: 'c', children: [] }] }
  ]
};

function visit(node) {
  console.log(node.value);
  node.children.forEach(visit);
}

visit(tree);

Closures

Closures capture variables from outer scopes, enabling private state and configurable functions.

Private State
function makeCounter() {
  let count = 0;
  return () => {
    count++;
    return count;
  };
}

const next = makeCounter();
console.log(next());
console.log(next());
Function Factory
function makeMultiplier(factor) {
  return number => number * factor;
}

const doubleNum = makeMultiplier(2);
const tripleNum = makeMultiplier(3);

console.log(doubleNum(5));
console.log(tripleNum(5));

Functions as First-Class Values

Pass functions as arguments, return them from other functions, and store them in data structures.

Higher-Order Functions
function applyTwice(fn, value) {
  return fn(fn(value));
}

const increment = n => n + 1;
console.log(applyTwice(increment, 3)); // 5
Storing Functions
const strategies = {
  json: data => JSON.stringify(data),
  text: data => String(data)
};

const format = (type, payload) => strategies[type]?.(payload) ?? '';
console.log(format('json', { ok: true }));
console.log(format('text', 42));

Practice Exercises

  1. Create a function declaration and a function expression that both greet a user; log their outputs.
  2. Write an arrow function that returns an object literal and test lexical this inside a method.
  3. Build an IIFE that initializes configuration and exposes only a getter.
  4. Implement a function with default parameters and rest parameters, then destructure its arguments.
  5. Write a recursive function to sum nested arrays (e.g., [1, [2, [3]]]).
  6. Create a closure-based counter with increment and reset capabilities.
  7. Return a function from another function that multiplies numbers by a chosen factor.
  8. Demonstrate early returns for invalid input in a form validation function.
  9. Use higher-order functions to apply a callback to a dataset and compare to inline logic.
  10. Document a function with JSDoc including parameter types and return type.
Key Takeaways:
  • Functions are first-class: declare, assign, pass, and return them freely.
  • Arrow functions are concise and capture this lexically; use them for callbacks and small utilities.
  • Defaults, rest parameters, and destructuring make APIs flexible and explicit.
  • Closures enable private state and function factories; always include base cases in recursion.
  • IIFEs set up isolated scopes, while early returns keep function bodies clear and intent-focused.

What's Next?

Proceed to Arrays to manipulate collections with the functions you write, or revisit Loops and Iteration to see how functions and iteration pair together.

Scope & Hoisting

Understand where variables live and how declarations move during compilation.

JavaScript scopes control visibility. Declarations are processed before execution (hoisting), but var behaves differently from let, const, and functions. Knowing lexical scope, the temporal dead zone, and closures prevents subtle bugs.

Global and Function Scope

var is function-scoped; let and const are block-scoped. Globals live on window in browsers.

Function Scope
var greeting = 'hello';

function run() {
  var greeting = 'hi';
  console.log(greeting);
}

run();
console.log(greeting);
Global Side Effects
function setFlag() {
  flag = true; // implicit global (avoid)
}

setFlag();
console.log(window.flag);

Block Scope

let and const respect block boundaries like if and for.

Block Visibility
if (true) {
  let scoped = 'inside';
  const fixed = 42;
}

// console.log(scoped); // ReferenceError
// console.log(fixed);  // ReferenceError
Loop Binding
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log('let', i), 0);
}

for (var j = 0; j < 3; j++) {
  setTimeout(() => console.log('var', j), 0);
}

Lexical Scope

Inner scopes can access outer variables; the reverse is not true.

Nested Access
const outer = 'outside';

function parent() {
  const mid = 'middle';
  function child() {
    const inner = 'inside';
    console.log(outer, mid, inner);
  }
  child();
}

parent();
Illegal Access
function parent() {
  const secret = 'hidden';
}

// console.log(secret); // ReferenceError

Hoisting Rules

Declarations are hoisted to the top of their scope. Initializations are not.

var Hoisting
console.log(total); // undefined due to hoisting
var total = 10;

// Behind the scenes:
// var total;
// console.log(total);
// total = 10;
let/const in TDZ
// console.log(count); // ReferenceError: TDZ
let count = 5;
const limit = 10;

console.log(count, limit);

Temporal Dead Zone (TDZ)

Between block start and declaration, let/const exist but cannot be accessed.

TDZ Demonstration
function demo(condition) {
  if (condition) {
    // console.log(message); // TDZ
    const message = 'ready';
    console.log(message);
  }
}

demo(true);

Function Hoisting

Function declarations are hoisted fully; function expressions obey variable hoisting.

Declaration
run();

function run() {
  console.log('runs');
}
Expression
// runLater(); // TypeError: runLater is not a function

var runLater = function() {
  console.log('later');
};

runLater();

Closures and Scope Chains

Closures capture lexical variables, enabling private state and deferred execution.

Private State
function makeCounter() {
  let value = 0;
  return function() {
    value++;
    return value;
  };
}

const inc = makeCounter();
console.log(inc());
console.log(inc());
Delayed Access
function fetchWithLog(url) {
  const started = Date.now();
  return async function() {
    const ms = Date.now() - started;
    console.log(`Requesting after ${ms}ms`);
    const res = await fetch(url);
    return res.json();
  };
}

const load = fetchWithLog('/api');
// later
// load();

Strict Mode and Accidental Globals

'use strict' prevents silent global creation and enforces safer semantics.

Strict Mode Guard
'use strict';

function markReady() {
  // status = 'ready'; // ReferenceError
  let status = 'ready';
  return status;
}

console.log(markReady());

Common Pitfalls

Avoid re-declaring var in the same scope and be aware of hoisted undefined values.

Shadowing Confusion
let value = 1;
function sample() {
  console.log(value);
  let value = 2; // TDZ hides outer value
}

// sample(); // ReferenceError
var Redeclaration
function configure() {
  var mode = 'a';
  var mode = 'b'; // allowed but risky
  return mode;
}

console.log(configure());

Practice Exercises

  1. Demonstrate the difference between var and let inside a for loop with setTimeout.
  2. Write a function that logs a variable before and after declaration using var, then repeat with let and compare errors.
  3. Create nested functions that show lexical scope access from inner to outer variables.
  4. Build a closure-based counter that exposes increment and reset; verify outer variables stay private.
  5. Enable strict mode in a function and attempt to assign to an undeclared variable to see the thrown error.
  6. Refactor a function that accidentally relies on hoisted undefined values into a safer version.
  7. Illustrate the temporal dead zone by accessing a let binding before declaration inside a block.
  8. Use destructuring with defaults inside a function parameter and observe how scope resolution works.
  9. Explain why function declarations are hoisted but function expressions may fail before initialization.
  10. Create a module-level constant and show how shadowing it inside a block changes only the inner scope.
Key Takeaways:
  • var is function-scoped; let/const are block-scoped and live in the temporal dead zone until initialized.
  • Hoisting moves declarations, not assignments; accessing too early yields undefined or ReferenceErrors.
  • Lexical scope builds a chain for closures, enabling private state and deferred execution.
  • Function declarations hoist fully; function expressions follow variable hoisting rules.
  • Strict mode stops accidental globals and surfaces errors earlier.

What's Next?

Move on to Prototypes and Inheritance to see how scope chains support object behavior, or revisit Destructuring, Spread, and Rest to practice reshaping scoped variables.

Arrays

Store ordered data, transform collections, and work with array-focused utilities.

Arrays are ordered lists that power most data flows in JavaScript. Learn to create them, access items, and use built-in methods for inserting, removing, slicing, and transforming. Mastering array helpers like map, filter, and reduce leads to cleaner, declarative data pipelines.

Creating and Accessing Arrays

Construct arrays with literals or constructors. Access elements by zero-based index, and use length to measure size.

Basics
const empty = [];
const numbers = [1, 2, 3];
const mixed = ['text', 99, true, { ok: true }];

console.log(numbers[0]); // 1
console.log(mixed[mixed.length - 1]); // { ok: true }
Spread from Iterables
const set = new Set([1, 2, 3]);
const fromSet = [...set];
const fromString = [...'hello'];

console.log(fromSet);
console.log(fromString);

Adding and Removing Items

Mutating methods adjust the array in place. Be aware when mutation is acceptable in your codebase.

push, pop, shift, unshift
const queue = ['first'];
queue.push('second');   // ['first', 'second']
queue.unshift('start'); // ['start', 'first', 'second']

const last = queue.pop();   // removes 'second'
const first = queue.shift(); // removes 'start'

console.log(queue, first, last);
splice for Flexible Edits
const tools = ['pencil', 'pen', 'marker'];
tools.splice(1, 1, 'eraser', 'ruler'); // replace pen with two items
console.log(tools); // ['pencil', 'eraser', 'ruler', 'marker']

tools.splice(2, 0, 'sharpener'); // insert without removal
console.log(tools);

tools.splice(-1, 1); // remove last item
console.log(tools);

Non-Mutating Copies

Prefer non-mutating methods when sharing arrays. slice, concat, and spread make safe copies.

slice and concat
const animals = ['cat', 'dog', 'bird', 'fish'];
const firstTwo = animals.slice(0, 2); // ['cat', 'dog']
const withoutFirst = animals.slice(1); // ['dog', 'bird', 'fish']

const combined = firstTwo.concat(['lizard']);
console.log(firstTwo, withoutFirst, combined);
console.log(animals); // original unchanged
Spread Copies
const base = [1, 2, 3];
const copy = [...base];
const merged = [...base, 4, ...[5, 6]];

console.log(copy, merged);
console.log(base);

Measuring and Checking

Use length to size arrays and helpers like includes and indexOf for membership tests.

Length and Includes
const tasks = ['draft', 'review', 'ship'];
console.log(tasks.length); // 3
console.log(tasks.includes('review')); // true
console.log(tasks.indexOf('ship')); // 2
console.log(tasks.indexOf('missing')); // -1
find and findIndex
const todos = [
  { id: 1, done: false },
  { id: 2, done: true }
];

const firstDone = todos.find(t => t.done);
const doneIndex = todos.findIndex(t => t.done);

console.log(firstDone, doneIndex);

Transforming Data

map, filter, and reduce create new collections or single values without mutation.

map and filter
const scores = [80, 92, 67];
const curved = scores.map(s => s + 5);
const passed = scores.filter(s => s >= 70);

console.log({ curved, passed });
reduce for Totals
const totals = scores.reduce((sum, s) => sum + s, 0);
const average = totals / scores.length;
console.log({ totals, average });

Searching and Checking

Locate elements quickly with find, some, and every. These short-circuit when possible.

some and every
const flags = [true, true, false];
console.log(flags.some(Boolean));  // true
console.log(flags.every(Boolean)); // false
find Usage
const catalog = [
  { sku: 'A1', stock: 0 },
  { sku: 'B2', stock: 3 },
  { sku: 'C3', stock: 5 }
];

const firstInStock = catalog.find(item => item.stock > 0);
console.log(firstInStock);

Sorting and Reversing

sort and reverse mutate arrays; copy first if you need to preserve originals.

Numeric Sort
const values = [10, 2, 30, 25];
const sorted = [...values].sort((a, b) => a - b);
const reversed = [...sorted].reverse();

console.log({ values, sorted, reversed });
Locale-Aware Sort
const names = ['Åsa', 'Anna', 'Özil'];
const collated = [...names].sort((a, b) => a.localeCompare(b));
console.log(collated);

Flattening and Mapping

Use flat and flatMap to compress nested arrays. Choose a depth that matches your data.

flat and flatMap
const nested = [1, [2, [3, 4]]];
console.log(nested.flat(1)); // [1, 2, [3, 4]]
console.log(nested.flat(2)); // [1, 2, 3, 4]

const products = [
  { name: 'pen', tags: ['stationery', 'office'] },
  { name: 'tape', tags: ['office'] }
];

const tags = products.flatMap(p => p.tags);
console.log(tags); // ['stationery', 'office', 'office']
Chaining Helpers
const logs = [
  'info:started',
  'warn:slow',
  'info:finished'
];

const warnings = logs
  .filter(line => line.startsWith('warn'))
  .map(line => line.split(':')[1])
  .flat();

console.log(warnings);

Copying, Cloning, and Immutability

Spread and slice copy arrays. Avoid mutating shared arrays unless necessary; prefer returning new arrays.

Immutable Updates
const state = ['todo'];
const nextState = [...state, 'in-progress'];
console.log(state, nextState);

// Remove without mutation
const filtered = nextState.filter(status => status !== 'todo');
console.log(filtered);

// Replace value immutably
const replaced = nextState.map(status => status === 'todo' ? 'done' : status);
console.log(replaced);
Deep-ish Clones
const deep = [{ id: 1, tags: ['a'] }];
const cloned = deep.map(item => ({ ...item, tags: [...item.tags] }));

cloned[0].tags.push('b');
console.log(deep[0].tags);   // ['a']
console.log(cloned[0].tags); // ['a', 'b']

Practice Exercises

  1. Create arrays using literals, constructors, and spread from a Set.
  2. Use push/pop/shift/unshift to manage a queue and log its state.
  3. Experiment with splice to insert, replace, and remove items at different positions.
  4. Slice an array into segments and recombine them with concat or spread.
  5. Transform a list with map and filter it with filter; compute totals with reduce.
  6. Find an object in an array with find and its position with findIndex.
  7. Sort numbers and strings; note when you need a comparator for correct numeric order.
  8. Flatten nested arrays with flat and merge mapping plus flattening with flatMap.
  9. Clone an array deeply enough to modify nested arrays without touching the original.
  10. Build a pipeline that chains filter, map, and reduce to summarize data.
Key Takeaways:
  • Arrays are ordered collections; access by index and track size with length.
  • Use mutating methods (push, pop, splice) cautiously; prefer non-mutating copies with slice, concat, or spread.
  • Transform data declaratively with map, filter, reduce, and search with find, some, every.
  • sort and reverse mutate; copy first if you need the original order.
  • flat and flatMap simplify nested structures; clone nested arrays to avoid unintended side effects.

What's Next?

Combine arrays with Functions to build reusable data pipelines, or revisit Control Flow to decide when those pipelines run.

Objects

Model real-world entities with properties, methods, and flexible shapes.

Objects are key-value maps that let you bundle related data and behavior. Learn literals, methods, the this keyword, computed properties, cloning patterns, and built-in helpers like Object.keys, Object.values, and Object.entries to work confidently with structured data.

Object Literals and Properties

Create objects with literal syntax and access properties using dot or bracket notation.

Creating and Reading
const user = {
    name: 'Alex',
    age: 28,
    role: 'editor',
    active: true
};

console.log(user.name);
console.log(user['role']);

user.location = 'Remote';
user['theme'] = 'dark';
Updating and Deleting
user.age = 29;
delete user.active;

console.log(Object.keys(user));

Methods and the this Keyword

Functions stored on objects are methods. this points to the owning object when using method syntax.

Defining Methods
const account = {
    owner: 'Jordan',
    balance: 500,
    deposit(amount) {
        this.balance += amount;
        return this.balance;
    },
    withdraw(amount) {
        if (amount > this.balance) return 'Insufficient';
        this.balance -= amount;
        return this.balance;
    }
};

console.log(account.deposit(150));
console.log(account.withdraw(200));
Losing Context
const withdraw = account.withdraw;
console.log(withdraw(50)); // this is undefined in strict mode

const safeWithdraw = account.withdraw.bind(account);
console.log(safeWithdraw(50));

Shorthand and Computed Properties

Use shorthand when property names match variable names, and compute keys dynamically.

Property Shorthand
const title = 'Engineer';
const level = 'Senior';

const profile = { title, level, location: 'NYC' };
console.log(profile);
Computed Keys
const metric = 'pageViews';
const stats = {
    [metric]: 1200,
    ['last-update']: new Date().toISOString()
};

console.log(stats.pageViews);
console.log(stats['last-update']);

Nested Objects and Optional Chaining

Model deeper structures and avoid crashes with optional chaining and nullish coalescing.

Nested Structure
const product = {
    id: 'sku-1',
    details: {
        name: 'Desk',
        price: 199,
        dimensions: { width: 120, height: 75 }
    },
    stock: {
        warehouse: 10,
        store: 2
    }
};

console.log(product.details.dimensions.width);
console.log(product.stock?.store ?? 0);
console.log(product.supplier?.name ?? 'Unknown');

Cloning and Merging

Use Object.assign or spread to copy and combine objects without mutating sources.

Shallow Copies
const base = { ready: true, retries: 0 };
const copyA = Object.assign({}, base, { retries: 1 });
const copyB = { ...base, retries: 2 };

console.log(copyA, copyB);
Merge Configs
const defaults = { cache: true, timeout: 3000 };
const overrides = { timeout: 5000, headers: { Accept: 'application/json' } };
const config = { ...defaults, ...overrides };

console.log(config);

Object Utilities

Iterate over keys and values to transform data structures.

keys, values, entries
const settings = { theme: 'dark', language: 'en', beta: false };
const keys = Object.keys(settings);
const values = Object.values(settings);
const entries = Object.entries(settings);

console.log(keys);
console.log(values);

const mapped = entries.map(([key, value]) => `${key}=${value}`);
console.log(mapped.join('; '));

Destructuring Objects

Pull properties into variables with defaults, renaming, and rest properties.

Basic Destructuring
const customer = { id: 7, name: 'Sam', plan: 'pro' };
const { name, plan } = customer;
console.log(name, plan);
Renaming and Defaults
const response = { status: 200 };
const { status: code, message = 'OK' } = response;

console.log(code, message);

const { plan: tier, ...rest } = customer;
console.log(tier, rest);

Immutability and Freezing

Prevent accidental mutations by freezing or creating new copies instead of altering originals.

Object.freeze
const env = Object.freeze({ mode: 'prod', version: '1.0.0' });
env.mode = 'dev';

console.log(env.mode); // still prod
Pure Updates
const addTag = (item, tag) => ({ ...item, tags: [...(item.tags ?? []), tag] });
const note = { title: 'Todo', tags: ['urgent'] };

console.log(addTag(note, 'today'));
console.log(note);

Object Factories and Patterns

Factories return new object instances with private state via closures.

Factory Function
function createCounter(label) {
    let value = 0;
    return {
        label,
        increment() {
            value++;
            return `${label}: ${value}`;
        },
        reset() {
            value = 0;
        }
    };
}

const visits = createCounter('visits');
console.log(visits.increment());
console.log(visits.increment());
visits.reset();
console.log(visits.increment());
Object Composition
const canLog = state => ({
    log(message) {
        state.history.push(message);
        return state.history;
    }
});

const canToggle = state => ({
    toggle() {
        state.enabled = !state.enabled;
        return state.enabled;
    }
});

const createFeature = name => {
    const state = { name, enabled: false, history: [] };
    return { ...state, ...canLog(state), ...canToggle(state) };
};

const feature = createFeature('Search');
console.log(feature.toggle());
console.log(feature.log('Initialized'));

Practice Exercises

  1. Create an object with shorthand properties and add a computed property for today's date.
  2. Write a method that uses this to update a balance; test what happens when the method is detached.
  3. Clone an object with spread, change a nested property safely, and compare to the original.
  4. Use Object.entries to convert an object to query-string format.
  5. Destructure an object with renaming and defaults, then use the rest property to collect remaining fields.
  6. Freeze a configuration object and attempt to mutate it; observe the result.
  7. Build a factory function that returns multiple counters sharing independent private state.
  8. Compose two capability mixins into a single object and verify both behaviors work.
  9. Write a helper that accepts an object of feature flags and returns only the enabled ones.
  10. Experiment with optional chaining on a nested object where an intermediate property is missing.
Key Takeaways:
  • Objects store key-value pairs, including methods that use this for context.
  • Shorthand and computed properties keep object literals concise and dynamic.
  • Spread and Object.assign create shallow copies; avoid mutating originals.
  • Object.keys, Object.values, and Object.entries help iterate and transform objects.
  • Destructuring with defaults, rest, and renaming simplifies property access.
  • Freezing objects enforces immutability; factories and composition build reusable behaviors.

What's Next?

Continue with Destructuring, Spread, and Rest to unpack and reassemble data, or revisit Functions to deepen how behavior is attached to objects.

Destructuring, Spread & Rest

Unpack, clone, and recombine data with expressive ES6 syntax.

Destructuring lets you pull values from arrays and objects into variables. Spread copies elements into new collections, while rest gathers remaining items. Master these together to simplify assignments, function parameters, and data transformations.

Array Destructuring Basics

Assign array items to variables by position. Skip items with commas and set defaults for missing values.

Simple Unpacking
const colors = ['red', 'green', 'blue'];
const [primary, secondary, tertiary] = colors;

console.log(primary, secondary, tertiary);
Skipping and Defaults
const response = ['ok'];
const [status, message = 'No message'] = response;
const [, secondColor] = ['cyan', 'magenta', 'yellow'];

console.log(status, message);
console.log(secondColor);

Swapping and Nested Destructuring

Swap variables without a temp and unpack nested structures in one statement.

Swap Values
let left = 'A';
let right = 'B';

[left, right] = [right, left];
console.log(left, right);
Nested Arrays
const grid = [[1, 2], [3, 4]];
const [[topLeft], [, bottomRight]] = grid;

console.log(topLeft, bottomRight);

Object Destructuring

Pick properties by name, rename them locally, and provide defaults.

Basics and Renaming
const user = { id: 9, name: 'Kai', role: 'admin' };
const { name, role: jobTitle } = user;

console.log(name, jobTitle);
Defaults and Nested
const settings = { theme: 'dark', layout: { sidebar: true } };
const { theme = 'light', layout: { sidebar = false, direction = 'ltr' } } = settings;

console.log(theme, sidebar, direction);

Rest Operator

Collect remaining items from arrays or properties from objects.

Array Rest
const [first, ...others] = ['ui', 'api', 'db', 'ops'];
console.log(first);
console.log(others);
Object Rest
const team = { lead: 'Sam', qa: 'Lee', dev: 'Ash', pm: 'Jo' };
const { lead, ...crew } = team;

console.log(lead);
console.log(crew);

Spread Operator

Expand arrays or objects into new ones for cloning, merging, and inserting.

Arrays
const baseStack = ['HTML', 'CSS'];
const stack = [...baseStack, 'JavaScript', 'TypeScript'];

const copy = [...stack];
console.log(stack);
console.log(copy);
Objects
const defaults = { retry: 3, cache: true };
const env = { cache: false };
const config = { ...defaults, ...env, headers: { Accept: 'application/json' } };

console.log(config);

Function Parameters

Destructure parameters to document required fields and apply defaults at the call boundary.

Object Params
function createUser({ name, role = 'viewer', active = true }) {
  return { name, role, active };
}

console.log(createUser({ name: 'Mira' }));
Array Params
function logTopTwo([first, second]) {
  console.log('Top:', first, second);
}

logTopTwo(['alpha', 'beta', 'gamma']);

Practical Transformations

Combine destructuring, spread, and rest for expressive data manipulation.

API Response Handling
const apiResponse = {
  data: { items: [{ id: 1 }, { id: 2 }], total: 2 },
  meta: { page: 1, pageSize: 10 }
};

const {
  data: { items, total },
  meta: { page }
} = apiResponse;

console.log(items, total, page);
Merging Lists
const core = ['login', 'logout'];
const extras = ['profile', 'billing'];

const features = ['home', ...core, 'search', ...extras];
const [, firstFeature, ...restFeatures] = features;

console.log(features);
console.log(firstFeature, restFeatures);

Safe Defaults and Guarding

Provide defaults when destructuring possibly undefined values to avoid runtime errors.

Defaulting Undefined
const config = null;
const { mode = 'prod', retries = 1 } = config ?? {};

console.log(mode, retries);
Nested Defaults
const payload = {};
const {
  user: {
    name = 'guest',
    preferences: { theme = 'light' } = {}
  } = {}
} = payload;

console.log(name, theme);

Patterns to Avoid

Use rest on the last position only and be mindful of shallow copies with spread.

Shallow Pitfall
const original = { nested: { count: 1 } };
const clone = { ...original };

clone.nested.count = 5;
console.log(original.nested.count); // 5 because spread is shallow

Destructuring in Loops and Params

Destructure directly in loop headers or parameter lists to keep bodies focused.

Loop Destructuring
const entries = [
  ['theme', 'dark'],
  ['lang', 'en']
];

for (const [key, value] of entries) {
  console.log(key, value);
}
Nested Params
function renderUser({ profile: { name }, stats: { followers = 0 } = {} }) {
  return `${name} (${followers} followers)`;
}

console.log(renderUser({ profile: { name: 'Mona' }, stats: { followers: 10 } }));

Practice Exercises

  1. Destructure the first two items of an array and gather the rest; log all three results.
  2. Swap two variables using array destructuring without creating a third variable.
  3. Destructure a nested object that may be missing keys using defaults and optional chaining.
  4. Use spread to clone an array and insert an item in the middle position.
  5. Write a function that destructures its object parameter and sets defaults for missing options.
  6. Combine two objects with spread, then override a nested field safely without mutating sources.
  7. Convert an array of entries back into an object after destructuring each pair.
  8. Demonstrate that object spread is shallow by mutating a nested value and observing both objects.
  9. Use rest parameters to capture extra arguments from a function call.
  10. Extract the first and last elements of an array while gathering the middle items into another array.
Key Takeaways:
  • Destructuring unpacks arrays by position and objects by name, with support for defaults and renaming.
  • Rest gathers remaining elements or properties; spread expands collections for cloning and merging.
  • Use destructuring in parameters to document required fields and keep call sites clean.
  • Spread and rest are shallow; nested objects or arrays still share references.
  • Swap values, reshape responses, and provide safe defaults with concise syntax.

What's Next?

Head to Scope and Hoisting to see where variables live, or jump back to Objects to practice combining these patterns with real data structures.

Template Literals

Modern string interpolation and tagged templates

Introduction: Template literals (introduced in ES6) revolutionize string handling in JavaScript with backtick syntax (`). They provide multi-line strings, expression interpolation, and tagged templates for advanced string processing. Template literals make code more readable and enable powerful features like HTML templating, SQL query builders, and custom string processors without messy concatenation.

Multi-line Strings

Create strings that span multiple lines without escape characters or concatenation.

Multi-line String Syntax
// Old way - difficult to read
const oldMultiline = 'This is line 1\n' +
  'This is line 2\n' +
  'This is line 3';

// Template literals - clean and intuitive
const newMultiline = `This is line 1
This is line 2
This is line 3`;

console.log(newMultiline);

// HTML templates
const htmlTemplate = `
  

Welcome

This is a paragraph

`; // SQL queries const sqlQuery = ` SELECT users.name, orders.total FROM users INNER JOIN orders ON users.id = orders.user_id WHERE orders.status = 'completed' ORDER BY orders.total DESC `; // Email templates const emailBody = ` Dear Customer, Thank you for your order! Your order will be delivered soon. Best regards, The Team `; // Code snippets const codeExample = ` function greet(name) { return \`Hello, \${name}!\`; } `; // Preserves indentation const indented = ` Level 1 Level 2 Level 3 `; console.log(indented); // Indentation is preserved // Remove leading indentation function dedent(str) { const lines = str.split('\n'); const minIndent = Math.min( ...lines .filter(line => line.trim()) .map(line => line.search(/\S/)) ); return lines .map(line => line.slice(minIndent)) .join('\n') .trim(); } const code = ` function hello() { console.log('world'); } `; console.log(dedent(code));

Expression Interpolation

Embed expressions directly in strings with ${} syntax for dynamic content.

String Interpolation Examples
// Basic interpolation
const name = 'John';
const age = 30;

console.log(`Hello, ${name}!`);
console.log(`${name} is ${age} years old`);

// Expressions
const a = 10;
const b = 20;
console.log(`The sum is ${a + b}`); // 'The sum is 30'
console.log(`Double: ${a * 2}`); // 'Double: 20'

// Function calls
function getGreeting() {
  return 'Good morning';
}

console.log(`${getGreeting()}, ${name}!`);

// Method calls
const user = {
  firstName: 'John',
  lastName: 'Doe',
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
};

console.log(`Welcome, ${user.fullName()}!`);

// Ternary operators
const isAdmin = true;
console.log(`Role: ${isAdmin ? 'Administrator' : 'User'}`);

const score = 85;
console.log(`Grade: ${score >= 90 ? 'A' : score >= 80 ? 'B' : 'C'}`);

// Object properties
const product = {
  name: 'Laptop',
  price: 999,
  discount: 0.1
};

console.log(`${product.name}: $${product.price * (1 - product.discount)}`);

// Array operations
const numbers = [1, 2, 3, 4, 5];
console.log(`Sum: ${numbers.reduce((a, b) => a + b, 0)}`);
console.log(`Items: ${numbers.join(', ')}`);

// Date formatting
const now = new Date();
console.log(`Today is ${now.toLocaleDateString()}`);

// Template in template
const nested = `Outer ${`Inner ${name}`}`;
console.log(nested); // 'Outer Inner John'

// Conditional rendering
const showDetails = true;
const message = `
  Name: ${name}
  ${showDetails ? `Age: ${age}` : ''}
`;

// Complex expressions
const items = ['apple', 'banana', 'orange'];
const list = `
  
    ${items.map(item => `
  • ${item}
  • `).join('\n ')}
`; console.log(list);

Nested Templates

Combine multiple template literals for complex data structures and UI components.

Nested Template Patterns
// Component pattern
function Card({ title, content, footer }) {
  return `
    

${title}

${content}
${footer ? ` ` : ''}
`; } const card = Card({ title: 'Welcome', content: 'This is the content', footer: 'Last updated: Today' }); // List rendering function UserList(users) { return `
    ${users.map(user => `
  • ${user.name} ${user.email}
  • `).join('')}
`; } const users = [ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' } ]; console.log(UserList(users)); // Table generation function Table(data, columns) { return ` ${columns.map(col => ``).join('')} ${data.map(row => ` ${columns.map(col => ``).join('')} `).join('')}
${col.label}
${row[col.key]}
`; } const tableData = [ { name: 'John', age: 30, city: 'NYC' }, { name: 'Jane', age: 25, city: 'LA' } ]; const tableColumns = [ { key: 'name', label: 'Name' }, { key: 'age', label: 'Age' }, { key: 'city', label: 'City' } ]; console.log(Table(tableData, tableColumns)); // Navigation menu function Nav(items) { return ` `; } const navItems = [ { label: 'Home', url: '/', active: true }, { label: 'Products', url: '/products', children: [ { label: 'Electronics', url: '/products/electronics' }, { label: 'Clothing', url: '/products/clothing' } ] } ]; console.log(Nav(navItems));

Tagged Templates

Tagged templates allow custom processing of template literals with tag functions.

Tagged Template Functions
// Basic tagged template
function tag(strings, ...values) {
  console.log('Strings:', strings); // Array of string parts
  console.log('Values:', values);   // Array of interpolated values

  return strings.reduce((result, str, i) => {
    return result + str + (values[i] || '');
  }, '');
}

const name = 'John';
const age = 30;
const result = tag`Hello ${name}, you are ${age} years old`;

// Uppercase interpolations
function uppercase(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] ? String(values[i]).toUpperCase() : '');
  }, '');
}

console.log(uppercase`Hello ${name}!`); // 'Hello JOHN!'

// Currency formatting
function currency(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i];
    const formatted = typeof value === 'number'
      ? `$${value.toFixed(2)}`
      : value || '';
    return result + str + formatted;
  }, '');
}

const price = 19.99;
console.log(currency`The price is ${price}`); // 'The price is $19.99'

// Safe HTML escaping
function html(strings, ...values) {
  const escape = (str) => {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
  };

  return strings.reduce((result, str, i) => {
    const value = values[i];
    const escaped = value != null ? escape(String(value)) : '';
    return result + str + escaped;
  }, '');
}


const safe = html`
${userInput}
`; console.log(safe); // Escaped, safe HTML // Styled components pattern function css(strings, ...values) { return strings.reduce((result, str, i) => { return result + str + (values[i] || ''); }, ''); } const primaryColor = '#007bff'; const styles = css` .button { background-color: ${primaryColor}; padding: 10px 20px; border-radius: 4px; } `; // Localization/i18n function t(strings, ...values) { // Simplified translation function const translations = { 'Hello': 'Hola', 'Goodbye': 'Adiós' }; return strings.reduce((result, str, i) => { const translated = translations[str.trim()] || str; return result + translated + (values[i] || ''); }, ''); } console.log(t`Hello ${name}!`); // SQL query builder (NEVER use with user input!) function sql(strings, ...values) { // This is for demonstration only - use parameterized queries in production! return strings.reduce((query, str, i) => { const value = values[i]; const escaped = typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : value; return query + str + (escaped || ''); }, ''); } const userId = 123; const query = sql`SELECT * FROM users WHERE id = ${userId}`; // Debug logger function debug(strings, ...values) { const timestamp = new Date().toISOString(); const message = strings.reduce((result, str, i) => { return result + str + (values[i] !== undefined ? JSON.stringify(values[i]) : ''); }, ''); console.log(`[${timestamp}] DEBUG: ${message}`); return message; } debug`User ${name} logged in at ${new Date()}`;

HTML Escaping and Security

Prevent XSS attacks by properly escaping user-generated content in templates.

Safe HTML Templating
// Escape HTML entities
function escapeHTML(str) {
  const escapeMap = {
    '&': '&',
    '<': '<',
    '>': '>',
    '"': '"',
    "'": ''',
    '/': '/'
  };

  return String(str).replace(/[&<>"'/]/g, (char) => escapeMap[char]);
}

// Safe template tag
function safeHTML(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i];
    const escaped = value != null ? escapeHTML(value) : '';
    return result + str + escaped;
  }, '');
}

// DANGEROUS - Never do this!

// const dangerous = `
${userComment}
`; // SAFE - Always escape user input const safe = safeHTML`
${userComment}
`; console.log(safe); //
<script>alert("XSS")</script>
// Allow specific HTML tags function sanitizeHTML(html, allowedTags = ['b', 'i', 'em', 'strong']) { const div = document.createElement('div'); div.innerHTML = html; // Remove all elements except allowed const allElements = div.querySelectorAll('*'); allElements.forEach(el => { if (!allowedTags.includes(el.tagName.toLowerCase())) { el.replaceWith(document.createTextNode(el.textContent)); } }); return div.innerHTML; } console.log(sanitizeHTML(mixedHTML)); // 'Bold alert("XSS") Italic' // URL encoding for attributes function encodeURL(url) { return encodeURIComponent(url); } function safeLink(url, text) { return `${escapeHTML(text)}`; } const userURL = 'javascript:alert("XSS")'; console.log(safeLink(userURL, 'Click me')); // Safe // Content Security Policy headers const cspTemplate = ` Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; `; // Safe JSON embedding function safeJSON(data) { return JSON.stringify(data) .replace(//g, '\\u003e') .replace(/&/g, '\\u0026'); } const safeScript = ` `;

SQL and CSS Injection Prevention

Protect against injection attacks in SQL queries and CSS styles.

Injection Prevention
// SQL injection prevention (ALWAYS use parameterized queries in production!)
function safeSQLParam(value) {
  if (typeof value === 'number') {
    return value;
  }
  if (typeof value === 'string') {
    // Escape single quotes
    return `'${value.replace(/'/g, "''")}'`;
  }
  if (value === null) {
    return 'NULL';
  }
  throw new Error('Unsupported type');
}

// DON'T DO THIS - vulnerable to SQL injection
// const username = "admin' OR '1'='1";
// const badQuery = `SELECT * FROM users WHERE username = '${username}'`;

// DO THIS - use parameterized queries
// const safeQuery = db.query('SELECT * FROM users WHERE username = ?', [username]);

// CSS injection prevention
function safeCSS(value) {
  // Remove potentially dangerous characters
  return String(value).replace(/[<>"';\(\)]/g, '');
}

function generateStyle(color, size) {
  return `
    .custom {
      color: ${safeCSS(color)};
      font-size: ${safeCSS(size)}px;
    }
  `;
}

// JavaScript code injection prevention
function safeJavaScript(value) {
  // Escape quotes and newlines
  return String(value)
    .replace(/\\/g, '\\\\')
    .replace(/'/g, "\\'")
    .replace(/"/g, '\\"')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r');
}

function generateScript(message) {
  return `
    
  `;
}

// Whitelist validation
function validateColor(color) {
  const validColors = ['red', 'blue', 'green', 'black', 'white'];
  return validColors.includes(color) ? color : 'black';
}

// Pattern validation
function validateHexColor(color) {
  return /^#[0-9A-Fa-f]{6}$/.test(color) ? color : '#000000';
}

// Safe URL construction
function buildURL(base, params) {
  const url = new URL(base);
  Object.keys(params).forEach(key => {
    url.searchParams.append(key, params[key]);
  });
  return url.toString();
}

const safeURL = buildURL('https://api.example.com/search', {
  q: 'user input',
  page: 1
});

Real-World Template Patterns

Production-ready patterns for component libraries and templating systems.

Advanced Template Patterns
// Template cache for performance
const templateCache = new Map();

function cachedTemplate(key, generator) {
  if (templateCache.has(key)) {
    return templateCache.get(key);
  }

  const template = generator();
  templateCache.set(key, template);
  return template;
}

// Component system
class Component {
  constructor(props) {
    this.props = props;
  }

  render() {
    return `
Override this method
`; } mount(selector) { const container = document.querySelector(selector); container.innerHTML = this.render(); } } class Button extends Component { render() { const { label, onClick, variant = 'primary' } = this.props; return ` `; } } // Usage const button = new Button({ label: 'Click Me', onClick: 'handleClick()', variant: 'success' }); button.mount('#app'); // Template inheritance function Layout(title, content) { return ` ${title}
Site Header
${content}
Site Footer
`; } function Page(title, body) { return Layout(title, `

${title}

${body}
`); } // Conditional rendering helper function renderIf(condition, template) { return condition ? template : ''; } // Loop rendering helper function renderEach(items, template) { return items.map(template).join(''); } // Usage const todos = [ { id: 1, text: 'Learn JS', done: true }, { id: 2, text: 'Build app', done: false } ]; const todoList = `
    ${renderEach(todos, todo => `
  • ${todo.text} ${renderIf(todo.done, ``)}
  • `)}
`; // Template composition function compose(...templates) { return templates.join('\n'); } const page = compose( '
Header
', '
Content
', '
Footer
' );

Practice Exercises

  1. Email Template: Create a tagged template for generating HTML emails with styling
  2. Markdown Renderer: Build a simple markdown-to-HTML converter using templates
  3. Form Generator: Create a function that generates HTML forms from configuration objects
  4. Table Builder: Build a reusable table component with sorting and filtering
  5. Safe HTML Tag: Implement a tagged template that automatically escapes user input
  6. CSS-in-JS: Create a styled components-like system using tagged templates
Key Takeaways:
  • Template literals use backticks (`) and enable multi-line strings
  • Use ${expression} for interpolation - any JavaScript expression works
  • Tagged templates allow custom processing with tag functions
  • Always escape user-generated content to prevent XSS attacks
  • Template literals preserve whitespace and indentation
  • Nest templates for complex component structures
  • Tagged templates power libraries like styled-components and lit-html
  • Prefer template literals over string concatenation for readability
What's Next? Continue your learning journey:

DOM Manipulation Basics

Master the Document Object Model and dynamic page updates

Introduction: The Document Object Model (DOM) is a programming interface that represents HTML documents as a tree structure. JavaScript can interact with this tree to dynamically read, modify, and create elements, enabling interactive web applications. Mastering DOM manipulation is fundamental to front-end development, from simple updates to complex dynamic interfaces.

Selecting Elements

Modern methods like querySelector and querySelectorAll provide powerful, flexible element selection using CSS selectors.

Element Selection Methods
// querySelector - returns first match
const header = document.querySelector('h1');
const button = document.querySelector('#submitBtn');
const firstItem = document.querySelector('.list-item');
const nestedElement = document.querySelector('div.container > p');

// querySelectorAll - returns NodeList of all matches
const allButtons = document.querySelectorAll('button');
const allItems = document.querySelectorAll('.list-item');

// Convert NodeList to Array for array methods
const itemsArray = Array.from(allItems);
const itemsSpread = [...allItems];

itemsArray.forEach(item => console.log(item.textContent));

// Legacy methods (still useful)
const byId = document.getElementById('myId'); // Faster than querySelector
const byClass = document.getElementsByClassName('myClass'); // Live HTMLCollection
const byTag = document.getElementsByTagName('div'); // Live HTMLCollection

// Difference: querySelector returns static, getElements returns live
const liveList = document.getElementsByClassName('item');
const staticList = document.querySelectorAll('.item');

console.log(liveList.length); // e.g., 3

document.body.innerHTML += '
New
'; console.log(liveList.length); // 4 (updated automatically) console.log(staticList.length); // Still 3 (static snapshot) // Complex selectors const complexSelect = document.querySelector('ul li:nth-child(2)'); const attributeSelect = document.querySelector('input[type="email"]'); const notSelector = document.querySelectorAll('div:not(.exclude)'); // Selecting within elements (scoped queries) const container = document.querySelector('.container'); const innerButton = container.querySelector('button'); // Only searches within container // Closest - find nearest ancestor matching selector const listItem = document.querySelector('.item'); const parentList = listItem.closest('ul'); const parentContainer = listItem.closest('.container'); // Matches - check if element matches selector if (button.matches('.primary')) { console.log('This is a primary button'); }

Creating and Removing Elements

Dynamically create, insert, and remove DOM elements to build interactive interfaces.

Element Creation and Manipulation
// Create elements
const div = document.createElement('div');
const paragraph = document.createElement('p');
const span = document.createElement('span');

// Set content and attributes
div.className = 'container';
div.id = 'myContainer';
paragraph.textContent = 'Hello, World!';

// Append to DOM
document.body.appendChild(div);
div.appendChild(paragraph);

// Modern methods: append (can take multiple nodes and strings)
div.append(span, ' More text', paragraph);

// prepend - add to beginning
div.prepend('First: ');

// before/after - insert before/after element
const existingEl = document.querySelector('#existing');
existingEl.before(div);
existingEl.after(span);

// replaceWith - replace element
const oldEl = document.querySelector('.old');
const newEl = document.createElement('div');
newEl.textContent = 'New element';
oldEl.replaceWith(newEl);

// insertAdjacentHTML - insert HTML at specific position
const list = document.querySelector('ul');

list.insertAdjacentHTML('beforebegin', '

List Title

'); list.insertAdjacentHTML('afterbegin', '
  • First Item
  • '); list.insertAdjacentHTML('beforeend', '
  • Last Item
  • '); list.insertAdjacentHTML('afterend', '

    After list

    '); // insertAdjacentElement and insertAdjacentText also available const newItem = document.createElement('li'); newItem.textContent = 'Inserted Item'; list.insertAdjacentElement('beforeend', newItem); // Remove elements const elementToRemove = document.querySelector('.remove-me'); elementToRemove.remove(); // Modern // Legacy removal // elementToRemove.parentNode.removeChild(elementToRemove); // Remove all children const parent = document.querySelector('.parent'); parent.innerHTML = ''; // Simple but loses event listeners // OR while (parent.firstChild) { parent.removeChild(parent.firstChild); // Safer } // OR parent.replaceChildren(); // Modern, clean way // Clone elements const original = document.querySelector('.original'); const clone = original.cloneNode(true); // true = deep clone (with children) document.body.appendChild(clone); // Create document fragment for batch operations const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const li = document.createElement('li'); li.textContent = `Item ${i}`; fragment.appendChild(li); // Doesn't trigger reflow } // Single DOM operation document.querySelector('ul').appendChild(fragment);

    innerHTML vs textContent

    Understanding the difference is crucial for security and performance.

    Content Manipulation Methods
    const element = document.querySelector('.content');
    
    // textContent - plain text only (SAFE)
    element.textContent = 'Hello, World!';
    element.textContent = 'Bold'; // Displays as plain text
    console.log(element.textContent); // Gets all text content
    
    // innerHTML - parses HTML (XSS risk!)
    element.innerHTML = 'Bold'; // Renders as HTML
    element.innerHTML = '

    Paragraph

    Another

    '; // DANGER: Never use user input directly const userInput = ''; // element.innerHTML = userInput; // DON'T DO THIS! // Safe alternative - escape HTML function escapeHTML(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } const safeHTML = escapeHTML(userInput); element.innerHTML = safeHTML; // Now safe // innerText vs textContent element.textContent = 'Some text'; // Faster, doesn't trigger reflow element.innerText = 'Some text'; // Respects CSS, slower // Example difference const hidden = document.querySelector('.hidden'); // CSS: display: none console.log(hidden.textContent); // Returns text console.log(hidden.innerText); // Returns empty string // outerHTML - includes element itself console.log(element.outerHTML); //
    ...
    element.outerHTML = '
    New element
    '; // Replaces element // insertAdjacentHTML for safer HTML insertion function addNotification(message) { const container = document.querySelector('#notifications'); const safeMessage = escapeHTML(message); container.insertAdjacentHTML('beforeend', `
    ${safeMessage}
    `); } // Performance comparison const container = document.querySelector('.container'); // SLOW: Multiple reflows for (let i = 0; i < 1000; i++) { container.innerHTML += `
    ${i}
    `; } // FAST: Single reflow let html = ''; for (let i = 0; i < 1000; i++) { html += `
    ${i}
    `; } container.innerHTML = html; // FASTEST: DOM methods with fragment const frag = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { const div = document.createElement('div'); div.textContent = i; frag.appendChild(div); } container.appendChild(frag);

    classList and Class Management

    Manipulate CSS classes efficiently with the classList API.

    Class Management
    const element = document.querySelector('.box');
    
    // Add classes
    element.classList.add('active');
    element.classList.add('highlighted', 'important'); // Multiple at once
    
    // Remove classes
    element.classList.remove('inactive');
    element.classList.remove('old', 'deprecated');
    
    // Toggle class
    element.classList.toggle('open'); // Adds if absent, removes if present
    
    // Toggle with condition
    const isActive = true;
    element.classList.toggle('active', isActive); // Adds if true, removes if false
    
    // Check if class exists
    if (element.classList.contains('active')) {
      console.log('Element is active');
    }
    
    // Replace class
    element.classList.replace('old-theme', 'new-theme');
    
    // Get all classes
    console.log(element.classList); // DOMTokenList
    console.log([...element.classList]); // Array of class names
    
    // Iterate classes
    element.classList.forEach(className => {
      console.log(className);
    });
    
    // Legacy className (less convenient)
    element.className = 'box active'; // Replaces all classes
    element.className += ' new-class'; // Append (watch the space!)
    console.log(element.className); // String of space-separated classes
    
    // Practical: State management
    class Component {
      constructor(element) {
        this.element = element;
      }
    
      setLoading(isLoading) {
        this.element.classList.toggle('loading', isLoading);
        this.element.classList.toggle('ready', !isLoading);
      }
    
      setError(hasError) {
        this.element.classList.toggle('error', hasError);
      }
    
      setState(states) {
        // Remove all state classes
        this.element.classList.remove('loading', 'success', 'error');
    
        // Add new state
        Object.keys(states).forEach(state => {
          if (states[state]) {
            this.element.classList.add(state);
          }
        });
      }
    }
    
    const component = new Component(document.querySelector('.widget'));
    component.setState({ loading: true });
    // Later...
    component.setState({ success: true });

    Attributes and Dataset

    Read and modify element attributes, including custom data attributes.

    Attribute Manipulation
    const link = document.querySelector('a');
    const input = document.querySelector('input');
    
    // Get attributes
    const href = link.getAttribute('href');
    const type = input.getAttribute('type');
    
    // Set attributes
    link.setAttribute('href', 'https://example.com');
    input.setAttribute('placeholder', 'Enter email');
    
    // Remove attributes
    link.removeAttribute('target');
    
    // Check if attribute exists
    if (link.hasAttribute('download')) {
      console.log('Download attribute present');
    }
    
    // Direct property access (preferred for standard attributes)
    link.href = 'https://example.com';
    input.value = 'test@example.com';
    input.disabled = true;
    
    // Boolean attributes
    input.disabled = true;
    input.required = true;
    input.readOnly = true;
    
    // Data attributes (custom attributes)
    // HTML: 
    const userEl = document.querySelector('[data-user-id]'); // Access via dataset (camelCase) console.log(userEl.dataset.userId); // "123" console.log(userEl.dataset.userName); // "John" // Set data attributes userEl.dataset.userRole = 'admin'; userEl.dataset.lastLogin = new Date().toISOString(); // Remove data attribute delete userEl.dataset.userName; // Practical: Store component state const button = document.querySelector('.toggle-btn'); button.dataset.expanded = 'false'; button.addEventListener('click', () => { const isExpanded = button.dataset.expanded === 'true'; button.dataset.expanded = !isExpanded; button.textContent = isExpanded ? 'Expand' : 'Collapse'; }); // Store complex data (serialize as JSON) const config = { theme: 'dark', lang: 'en', notifications: true }; element.dataset.config = JSON.stringify(config); // Retrieve complex data const retrievedConfig = JSON.parse(element.dataset.config); // Form input attributes const emailInput = document.querySelector('input[type="email"]'); emailInput.value = 'user@example.com'; emailInput.placeholder = 'Enter your email'; emailInput.required = true; emailInput.pattern = '[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$'; // Custom validation emailInput.setCustomValidity('Please enter a company email'); emailInput.setCustomValidity(''); // Clear custom validation // Get/set multiple attributes efficiently function setAttributes(element, attrs) { Object.keys(attrs).forEach(key => { element.setAttribute(key, attrs[key]); }); } setAttributes(link, { href: 'https://example.com', target: '_blank', rel: 'noopener noreferrer' });

    Event Listeners and Delegation

    Attach event handlers efficiently and use event delegation for dynamic elements.

    Event Handling
    const button = document.querySelector('#myButton');
    
    // Add event listener
    button.addEventListener('click', (event) => {
      console.log('Button clicked!');
      console.log('Event:', event);
      console.log('Target:', event.target);
    });
    
    // Multiple handlers for same event
    button.addEventListener('click', handler1);
    button.addEventListener('click', handler2);
    
    // Remove event listener (need reference to same function)
    function handleClick(e) {
      console.log('Clicked');
    }
    
    button.addEventListener('click', handleClick);
    button.removeEventListener('click', handleClick);
    
    // Event options
    button.addEventListener('click', handler, {
      once: true,      // Remove after first call
      passive: true,   // Won't call preventDefault
      capture: false   // Bubble phase (default)
    });
    
    // Prevent default behavior
    const link = document.querySelector('a');
    link.addEventListener('click', (e) => {
      e.preventDefault(); // Prevent navigation
      console.log('Link clicked but not followed');
    });
    
    // Stop propagation
    const parent = document.querySelector('.parent');
    const child = document.querySelector('.child');
    
    parent.addEventListener('click', () => console.log('Parent clicked'));
    child.addEventListener('click', (e) => {
      e.stopPropagation(); // Prevent bubble to parent
      console.log('Child clicked');
    });
    
    // Event delegation (efficient for dynamic elements)
    const list = document.querySelector('#todoList');
    
    // BAD: Adding listener to each item
    // document.querySelectorAll('.todo-item').forEach(item => {
    //   item.addEventListener('click', handleItemClick);
    // });
    
    // GOOD: Single listener on parent
    list.addEventListener('click', (e) => {
      const item = e.target.closest('.todo-item');
    
      if (item) {
        console.log('Todo clicked:', item.dataset.id);
    
        // Handle different clicked elements
        if (e.target.matches('.delete-btn')) {
          deleteTodo(item);
        } else if (e.target.matches('.edit-btn')) {
          editTodo(item);
        } else if (e.target.matches('.checkbox')) {
          toggleTodo(item);
        }
      }
    });
    
    function deleteTodo(item) {
      item.remove();
    }
    
    function editTodo(item) {
      const text = item.querySelector('.todo-text');
      text.contentEditable = true;
      text.focus();
    }
    
    function toggleTodo(item) {
      item.classList.toggle('completed');
    }
    
    // Common events
    const input = document.querySelector('input');
    
    input.addEventListener('input', (e) => {
      console.log('Value:', e.target.value); // On every keystroke
    });
    
    input.addEventListener('change', (e) => {
      console.log('Changed to:', e.target.value); // On blur
    });
    
    input.addEventListener('focus', () => console.log('Input focused'));
    input.addEventListener('blur', () => console.log('Input blurred'));
    
    // Form events
    const form = document.querySelector('form');
    
    form.addEventListener('submit', (e) => {
      e.preventDefault();
    
      const formData = new FormData(form);
      const data = Object.fromEntries(formData);
    
      console.log('Form data:', data);
    });
    
    // Keyboard events
    document.addEventListener('keydown', (e) => {
      console.log('Key:', e.key);
      console.log('Code:', e.code);
    
      if (e.ctrlKey && e.key === 's') {
        e.preventDefault();
        saveDocument();
      }
    });
    
    // Mouse events
    element.addEventListener('mouseenter', () => console.log('Mouse entered'));
    element.addEventListener('mouseleave', () => console.log('Mouse left'));
    element.addEventListener('mousemove', (e) => {
      console.log('Mouse position:', e.clientX, e.clientY);
    });

    DOM Traversal

    Navigate the DOM tree to find parent, child, and sibling elements.

    Traversing the DOM
    const element = document.querySelector('.current');
    
    // Parent traversal
    const parent = element.parentElement;
    const parentNode = element.parentNode; // Can be non-element nodes
    
    // Find nearest ancestor matching selector
    const container = element.closest('.container');
    const form = element.closest('form');
    
    // Children
    const children = element.children; // HTMLCollection of element children
    const firstChild = element.firstElementChild;
    const lastChild = element.lastElementChild;
    
    // All child nodes (including text nodes)
    const childNodes = element.childNodes; // NodeList
    
    // Siblings
    const nextSibling = element.nextElementSibling;
    const prevSibling = element.previousElementSibling;
    
    // All siblings
    function getSiblings(element) {
      return [...element.parentElement.children].filter(child => child !== element);
    }
    
    const siblings = getSiblings(element);
    
    // Recursive traversal
    function walkDOM(node, callback) {
      callback(node);
    
      node = node.firstChild;
      while (node) {
        walkDOM(node, callback);
        node = node.nextSibling;
      }
    }
    
    walkDOM(document.body, (node) => {
      if (node.nodeType === Node.ELEMENT_NODE) {
        console.log(node.tagName);
      }
    });
    
    // Find all elements of type
    function findElementsByType(root, tagName) {
      const elements = [];
    
      function traverse(node) {
        if (node.tagName === tagName.toUpperCase()) {
          elements.push(node);
        }
    
        [...node.children].forEach(traverse);
      }
    
      traverse(root);
      return elements;
    }
    
    const allDivs = findElementsByType(document.body, 'div');
    
    // Practical: Build breadcrumb trail
    function getBreadcrumb(element) {
      const breadcrumb = [];
      let current = element;
    
      while (current && current !== document.body) {
        breadcrumb.unshift({
          tag: current.tagName,
          id: current.id,
          class: current.className
        });
        current = current.parentElement;
      }
    
      return breadcrumb;
    }
    
    const trail = getBreadcrumb(document.querySelector('.deep-nested'));
    console.log(trail);
    
    // Check if element contains another
    const parent = document.querySelector('.parent');
    const child = document.querySelector('.child');
    
    if (parent.contains(child)) {
      console.log('Parent contains child');
    }
    
    // Get element index among siblings
    function getElementIndex(element) {
      return [...element.parentElement.children].indexOf(element);
    }
    
    const index = getElementIndex(element);
    console.log('Element is at index:', index);

    Practical DOM Patterns

    Real-world patterns and best practices for DOM manipulation.

    Production Patterns
    // Safe DOM ready
    function ready(fn) {
      if (document.readyState !== 'loading') {
        fn();
      } else {
        document.addEventListener('DOMContentLoaded', fn);
      }
    }
    
    ready(() => {
      console.log('DOM is ready');
      initializeApp();
    });
    
    // Component pattern
    class TodoList {
      constructor(container) {
        this.container = container;
        this.items = [];
        this.render();
        this.attachEvents();
      }
    
      render() {
        this.container.innerHTML = `
          
      `; this.input = this.container.querySelector('.todo-input'); this.itemsList = this.container.querySelector('.todo-items'); } attachEvents() { this.container.querySelector('.todo-add').addEventListener('click', () => { this.addItem(this.input.value); this.input.value = ''; }); this.itemsList.addEventListener('click', (e) => { if (e.target.matches('.delete')) { const id = e.target.closest('li').dataset.id; this.removeItem(id); } }); } addItem(text) { if (!text.trim()) return; const id = Date.now().toString(); this.items.push({ id, text }); const li = document.createElement('li'); li.dataset.id = id; li.innerHTML = ` ${text} `; this.itemsList.appendChild(li); } removeItem(id) { this.items = this.items.filter(item => item.id !== id); this.itemsList.querySelector(`[data-id="${id}"]`).remove(); } } // Usage const todoList = new TodoList(document.querySelector('#app')); // Batch DOM updates function batchUpdate(updates) { // Force synchronous layout const container = document.querySelector('.container'); // Detach from DOM (prevents reflows) const parent = container.parentElement; const nextSibling = container.nextElementSibling; parent.removeChild(container); // Make all updates updates.forEach(update => update()); // Reattach parent.insertBefore(container, nextSibling); } // Virtual DOM-like diffing (simplified) function updateList(container, newItems, oldItems) { const itemsToAdd = newItems.filter(item => !oldItems.find(old => old.id === item.id) ); const itemsToRemove = oldItems.filter(item => !newItems.find(newItem => newItem.id === item.id) ); // Remove itemsToRemove.forEach(item => { container.querySelector(`[data-id="${item.id}"]`)?.remove(); }); // Add itemsToAdd.forEach(item => { const el = document.createElement('div'); el.dataset.id = item.id; el.textContent = item.text; container.appendChild(el); }); }

      Practice Exercises

      1. Dynamic Table: Create a table component that renders data from an array and supports sorting, filtering
      2. Modal Dialog: Build a reusable modal with open/close animations and escape key handling
      3. Infinite Scroll: Implement infinite scrolling that loads more items as user scrolls to bottom
      4. Form Validator: Create real-time form validation with custom error messages and styling
      5. Drag and Drop: Build a drag-and-drop interface for reordering list items
      6. Tree View: Create an expandable/collapsible tree view component with nested items
      Key Takeaways:
      • Use querySelector/querySelectorAll for flexible, CSS-based element selection
      • textContent is safer than innerHTML for user-generated content (prevents XSS)
      • classList API provides clean class manipulation - prefer over className
      • Event delegation is more efficient than attaching listeners to many elements
      • Use dataset for custom data attributes with automatic camelCase conversion
      • Batch DOM updates and use DocumentFragment to minimize reflows
      • Always remove event listeners and cleanup to prevent memory leaks
      • Wait for DOMContentLoaded or place scripts at end of body
      What's Next? Continue your learning journey:

      Events & Event Loop

      Understanding JavaScript's asynchronous execution model

      Introduction: JavaScript's event loop is the secret behind its asynchronous, non-blocking nature. Despite being single-threaded, JavaScript handles multiple operations concurrently through an elegant system of call stacks, queues, and the event loop. Understanding this model is crucial for writing efficient, bug-free asynchronous code and avoiding common pitfalls like race conditions and memory leaks.

      Call Stack Fundamentals

      The call stack tracks function execution in Last-In-First-Out (LIFO) order. Understanding stack behavior is key to debugging.

      Call Stack Mechanics
      // Stack execution order
      function first() {
        console.log('First function');
        second();
        console.log('First function done');
      }
      
      function second() {
        console.log('Second function');
        third();
        console.log('Second function done');
      }
      
      function third() {
        console.log('Third function');
      }
      
      first();
      // Output:
      // First function
      // Second function
      // Third function
      // Second function done
      // First function done
      
      // Stack overflow example
      function recursiveWithoutBase() {
        recursiveWithoutBase(); // No base case!
      }
      
      // This will crash: Maximum call stack size exceeded
      // recursiveWithoutBase();
      
      // Proper recursion with base case
      function countdown(n) {
        if (n <= 0) return; // Base case
        console.log(n);
        countdown(n - 1);
      }
      
      countdown(5);
      
      // Stack trace visualization
      function a() {
        console.trace('Stack trace from a()');
        b();
      }
      
      function b() {
        console.trace('Stack trace from b()');
        c();
      }
      
      function c() {
        console.trace('Stack trace from c()');
      }
      
      a(); // Shows full stack trace at each level

      Event Loop Phases

      The event loop continuously checks the call stack and processes tasks from various queues in specific phases.

      Event Loop Process
      // Event loop visualization
      console.log('1. Synchronous');
      
      setTimeout(() => {
        console.log('2. Macrotask (timer)');
      }, 0);
      
      Promise.resolve().then(() => {
        console.log('3. Microtask (promise)');
      });
      
      console.log('4. Synchronous');
      
      // Output order:
      // 1. Synchronous
      // 4. Synchronous
      // 3. Microtask (promise)
      // 2. Macrotask (timer)
      
      // Detailed execution phases
      console.log('Start'); // Call stack
      
      setTimeout(() => {
        console.log('Timeout 1'); // Macrotask queue
        Promise.resolve().then(() => console.log('Promise in timeout'));
      }, 0);
      
      setTimeout(() => {
        console.log('Timeout 2'); // Macrotask queue
      }, 0);
      
      Promise.resolve()
        .then(() => {
          console.log('Promise 1'); // Microtask queue
          return Promise.resolve();
        })
        .then(() => console.log('Promise 2')); // Microtask queue
      
      console.log('End'); // Call stack
      
      // Output:
      // Start
      // End
      // Promise 1
      // Promise 2
      // Timeout 1
      // Promise in timeout
      // Timeout 2
      
      // Event loop never blocks
      function longRunningTask() {
        const start = Date.now();
        while (Date.now() - start < 3000) {
          // Blocks for 3 seconds - BAD!
        }
        console.log('Task done');
      }
      
      // This blocks the entire thread
      // longRunningTask();
      
      // Better: Break into chunks
      function chunkTask(items, chunkSize = 100) {
        let index = 0;
      
        function processChunk() {
          const end = Math.min(index + chunkSize, items.length);
      
          for (let i = index; i < end; i++) {
            // Process items[i]
            console.log(`Processing item ${i}`);
          }
      
          index = end;
      
          if (index < items.length) {
            setTimeout(processChunk, 0); // Let event loop breathe
          }
        }
      
        processChunk();
      }
      
      // Usage
      chunkTask(Array.from({ length: 1000 }, (_, i) => i));

      Callback Queue (Task Queue)

      The callback/task queue holds macrotasks like setTimeout, setInterval, and I/O operations.

      Macrotask Queue
      // Macrotasks are processed after microtasks
      console.log('Script start');
      
      setTimeout(() => {
        console.log('setTimeout 0');
      }, 0);
      
      setTimeout(() => {
        console.log('setTimeout 10');
      }, 10);
      
      setInterval(() => {
        console.log('setInterval - fires repeatedly');
      }, 1000);
      
      // I/O operations are also macrotasks
      fetch('https://api.example.com/data')
        .then(() => console.log('Fetch complete'));
      
      console.log('Script end');
      
      // Macrotask scheduling
      const tasks = [];
      
      function scheduleMacrotask(fn) {
        setTimeout(fn, 0);
      }
      
      scheduleMacrotask(() => console.log('Macrotask 1'));
      scheduleMacrotask(() => console.log('Macrotask 2'));
      scheduleMacrotask(() => console.log('Macrotask 3'));
      
      // Macrotasks don't block each other
      setTimeout(() => {
        console.log('First timeout starts');
      
        // Even with blocking code, other timeouts wait
        const start = Date.now();
        while (Date.now() - start < 2000) {}
      
        console.log('First timeout ends');
      }, 0);
      
      setTimeout(() => {
        console.log('Second timeout executes after first completes');
      }, 0);
      
      // Event listeners add to callback queue
      document.querySelector('#myButton')?.addEventListener('click', () => {
        console.log('Click handler - macrotask');
      
        Promise.resolve().then(() => {
          console.log('Microtask inside click handler');
        });
      
        console.log('Click handler continues');
      });
      
      // Output when clicked:
      // Click handler - macrotask
      // Click handler continues
      // Microtask inside click handler

      Microtask Queue (Promises)

      Microtasks (promises, queueMicrotask) have higher priority and run before the next macrotask.

      Microtask Priority
      // Microtasks always run before macrotasks
      setTimeout(() => console.log('Timeout'), 0);
      
      Promise.resolve()
        .then(() => console.log('Promise 1'))
        .then(() => console.log('Promise 2'))
        .then(() => console.log('Promise 3'));
      
      queueMicrotask(() => console.log('queueMicrotask'));
      
      console.log('Synchronous');
      
      // Output:
      // Synchronous
      // Promise 1
      // queueMicrotask
      // Promise 2
      // Promise 3
      // Timeout
      
      // Microtask chain doesn't block macrotasks
      Promise.resolve()
        .then(() => {
          console.log('Microtask 1');
          return Promise.resolve();
        })
        .then(() => {
          console.log('Microtask 2');
          setTimeout(() => console.log('Timeout in microtask'), 0);
        })
        .then(() => console.log('Microtask 3'));
      
      setTimeout(() => console.log('Macrotask'), 0);
      
      // Microtask infinite loop caution
      let count = 0;
      
      function scheduleRecursiveMicrotask() {
        queueMicrotask(() => {
          console.log(`Microtask ${++count}`);
      
          if (count < 1000000) {
            scheduleRecursiveMicrotask(); // This BLOCKS macrotasks!
          }
        });
      }
      
      // DON'T DO THIS - starves macrotasks
      // scheduleRecursiveMicrotask();
      
      // Better: Give macrotasks a chance
      function scheduleWithBreaks() {
        let count = 0;
      
        function schedule() {
          if (count < 100) {
            queueMicrotask(() => {
              console.log(`Task ${++count}`);
            });
            setTimeout(schedule, 0); // Let macrotasks run
          }
        }
      
        schedule();
      }
      
      // Async/await creates microtasks
      async function asyncFunction() {
        console.log('Async start');
      
        await Promise.resolve(); // Suspends, creates microtask
      
        console.log('After await'); // Runs as microtask
      }
      
      console.log('Before async');
      asyncFunction();
      console.log('After async call');
      
      // Output:
      // Before async
      // Async start
      // After async call
      // After await

      Execution Order Analysis

      Predict and understand complex execution order by tracking synchronous, microtask, and macrotask code.

      Complex Execution Order
      // Complex example
      console.log('1');
      
      setTimeout(() => {
        console.log('2');
        Promise.resolve().then(() => console.log('3'));
      }, 0);
      
      Promise.resolve()
        .then(() => {
          console.log('4');
          setTimeout(() => console.log('5'), 0);
        })
        .then(() => console.log('6'));
      
      console.log('7');
      
      // Output: 1, 7, 4, 6, 2, 3, 5
      
      // Step-by-step breakdown:
      // Initial (sync): 1, 7
      // Microtasks: 4, 6
      // Macrotasks: 2 (triggers microtask 3), then 5
      
      // Nested promises and timeouts
      setTimeout(() => {
        console.log('Timeout 1');
      
        Promise.resolve()
          .then(() => console.log('Promise 1'))
          .then(() => console.log('Promise 2'));
      
        setTimeout(() => console.log('Timeout 2'), 0);
      }, 0);
      
      Promise.resolve()
        .then(() => {
          console.log('Promise 3');
          setTimeout(() => console.log('Timeout 3'), 0);
        });
      
      // Real-world: Button click simulation
      function simulateComplexEvent() {
        console.log('Event triggered');
      
        // Sync work
        const data = processData();
        console.log('Data processed');
      
        // Async validation (microtask)
        Promise.resolve()
          .then(() => {
            console.log('Validation started');
            return validateData(data);
          })
          .then(isValid => {
            console.log('Validation complete:', isValid);
      
            if (isValid) {
              // API call (macrotask)
              setTimeout(() => {
                console.log('API call made');
              }, 0);
            }
          });
      
        console.log('Event handler done');
      }
      
      function processData() {
        return { value: 42 };
      }
      
      function validateData(data) {
        return data.value > 0;
      }
      
      // Debugging execution order
      function debugEventLoop() {
        const log = [];
      
        log.push('Start');
      
        setTimeout(() => log.push('Timeout 1'), 0);
      
        Promise.resolve().then(() => {
          log.push('Promise 1');
          return Promise.resolve();
        }).then(() => log.push('Promise 2'));
      
        setTimeout(() => {
          log.push('Timeout 2');
          console.log('Execution order:', log);
        }, 10);
      
        log.push('End');
      }
      
      debugEventLoop();

      Blocking Code and Performance

      Identify and fix blocking operations that freeze the UI and degrade user experience.

      Avoiding Blocking Code
      // BAD: Blocking operation
      function heavyCalculation() {
        let result = 0;
        for (let i = 0; i < 1000000000; i++) {
          result += i;
        }
        return result;
      }
      
      // This freezes the UI for seconds
      // const result = heavyCalculation();
      
      // GOOD: Break into chunks with setTimeout
      function heavyCalculationNonBlocking(callback) {
        let result = 0;
        let i = 0;
        const chunkSize = 10000000;
      
        function processChunk() {
          const end = Math.min(i + chunkSize, 1000000000);
      
          for (; i < end; i++) {
            result += i;
          }
      
          if (i < 1000000000) {
            setTimeout(processChunk, 0); // Yield to event loop
          } else {
            callback(result);
          }
        }
      
        processChunk();
      }
      
      heavyCalculationNonBlocking((result) => {
        console.log('Result:', result);
      });
      
      // BETTER: Use Web Workers (covered next)
      
      // Detect long-running tasks
      let lastTime = performance.now();
      
      setInterval(() => {
        const now = performance.now();
        const delta = now - lastTime;
      
        if (delta > 100) {
          console.warn(`Long task detected: ${delta}ms`);
        }
      
        lastTime = now;
      }, 50);
      
      // requestIdleCallback for non-critical work
      function deferredWork() {
        if ('requestIdleCallback' in window) {
          requestIdleCallback((deadline) => {
            while (deadline.timeRemaining() > 0) {
              // Do non-critical work
              console.log('Processing during idle time');
            }
          });
        } else {
          // Fallback
          setTimeout(deferredWork, 1);
        }
      }
      
      // Yield to browser periodically
      async function processLargeArray(items) {
        for (let i = 0; i < items.length; i++) {
          processItem(items[i]);
      
          // Yield every 100 items
          if (i % 100 === 0) {
            await new Promise(resolve => setTimeout(resolve, 0));
          }
        }
      }
      
      function processItem(item) {
        // Heavy processing
        console.log('Processing:', item);
      }

      Web Workers

      Offload heavy computations to background threads with Web Workers to keep the UI responsive.

      Web Workers for Background Processing
      // Main thread (main.js)
      const worker = new Worker('worker.js');
      
      // Send data to worker
      worker.postMessage({ type: 'calculate', data: [1, 2, 3, 4, 5] });
      
      // Receive results from worker
      worker.addEventListener('message', (e) => {
        console.log('Result from worker:', e.data);
      
        if (e.data.type === 'result') {
          displayResult(e.data.value);
        }
      });
      
      // Handle errors
      worker.addEventListener('error', (e) => {
        console.error('Worker error:', e.message);
      });
      
      // Terminate worker when done
      function cleanup() {
        worker.terminate();
      }
      
      // Worker thread (worker.js)
      // This code would be in a separate file
      /*
      self.addEventListener('message', (e) => {
        if (e.data.type === 'calculate') {
          const result = heavyCalculation(e.data.data);
      
          self.postMessage({
            type: 'result',
            value: result
          });
        }
      });
      
      function heavyCalculation(data) {
        let sum = 0;
        for (let i = 0; i < 1000000000; i++) {
          sum += data[i % data.length];
        }
        return sum;
      }
      */
      
      // Inline worker using Blob
      function createInlineWorker(fn) {
        const blob = new Blob([`(${fn.toString()})()`], {
          type: 'application/javascript'
        });
      
        return new Worker(URL.createObjectURL(blob));
      }
      
      const inlineWorker = createInlineWorker(() => {
        self.addEventListener('message', (e) => {
          const result = e.data.reduce((a, b) => a + b, 0);
          self.postMessage(result);
        });
      });
      
      inlineWorker.postMessage([1, 2, 3, 4, 5]);
      inlineWorker.addEventListener('message', (e) => {
        console.log('Sum:', e.data);
      });
      
      // Worker pool for multiple concurrent tasks
      class WorkerPool {
        constructor(workerScript, size = 4) {
          this.workers = [];
          this.queue = [];
      
          for (let i = 0; i < size; i++) {
            const worker = new Worker(workerScript);
            worker.busy = false;
      
            worker.addEventListener('message', (e) => {
              worker.busy = false;
              worker.callback(e.data);
              this.processQueue();
            });
      
            this.workers.push(worker);
          }
        }
      
        execute(data) {
          return new Promise((resolve) => {
            this.queue.push({ data, callback: resolve });
            this.processQueue();
          });
        }
      
        processQueue() {
          if (this.queue.length === 0) return;
      
          const availableWorker = this.workers.find(w => !w.busy);
          if (!availableWorker) return;
      
          const task = this.queue.shift();
          availableWorker.busy = true;
          availableWorker.callback = task.callback;
          availableWorker.postMessage(task.data);
        }
      
        terminate() {
          this.workers.forEach(w => w.terminate());
        }
      }
      
      // Usage
      const pool = new WorkerPool('heavy-task-worker.js', 4);
      
      Promise.all([
        pool.execute({ input: 1 }),
        pool.execute({ input: 2 }),
        pool.execute({ input: 3 })
      ]).then(results => {
        console.log('All tasks complete:', results);
      });

      Debugging Event Loop Issues

      Tools and techniques for diagnosing and fixing event loop related problems.

      Debugging Techniques
      // Visualize execution order
      function logWithPhase(message, phase) {
        const phases = {
          sync: '🟢',
          micro: '🔵',
          macro: '🔴'
        };
        console.log(`${phases[phase]} [${phase.toUpperCase()}] ${message}`);
      }
      
      logWithPhase('Start', 'sync');
      
      setTimeout(() => {
        logWithPhase('Timeout', 'macro');
      }, 0);
      
      Promise.resolve().then(() => {
        logWithPhase('Promise', 'micro');
      });
      
      logWithPhase('End', 'sync');
      
      // Performance monitoring
      class EventLoopMonitor {
        constructor() {
          this.longTasks = [];
          this.lastCheck = performance.now();
          this.start();
        }
      
        start() {
          this.checkInterval = setInterval(() => {
            const now = performance.now();
            const gap = now - this.lastCheck;
      
            // Expected: ~10ms, if > 50ms, something blocked
            if (gap > 50) {
              this.longTasks.push({
                duration: gap,
                timestamp: now
              });
              console.warn(`Long task: ${gap.toFixed(2)}ms`);
            }
      
            this.lastCheck = now;
          }, 10);
        }
      
        getReport() {
          return {
            totalLongTasks: this.longTasks.length,
            averageDuration: this.longTasks.reduce((a, b) => a + b.duration, 0) / this.longTasks.length,
            longestTask: Math.max(...this.longTasks.map(t => t.duration))
          };
        }
      
        stop() {
          clearInterval(this.checkInterval);
        }
      }
      
      const monitor = new EventLoopMonitor();
      
      // Simulate work and check report later
      setTimeout(() => {
        console.log('Report:', monitor.getReport());
        monitor.stop();
      }, 5000);
      
      // Trace async operations
      function traceAsync(name, fn) {
        console.log(`[${name}] Starting`);
      
        const result = fn();
      
        if (result && typeof result.then === 'function') {
          return result.then(
            (value) => {
              console.log(`[${name}] Resolved:`, value);
              return value;
            },
            (error) => {
              console.error(`[${name}] Rejected:`, error);
              throw error;
            }
          );
        }
      
        console.log(`[${name}] Completed:`, result);
        return result;
      }
      
      // Usage
      traceAsync('Fetch Users', () => {
        return fetch('/api/users').then(r => r.json());
      });

      Practice Exercises

      1. Execution Order Quiz: Write code with mixed sync/async/promises/timeouts and predict output before running
      2. Non-Blocking Sort: Implement a sorting algorithm that yields to the event loop every 1000 iterations
      3. Task Scheduler: Build a scheduler that runs tasks with priority (microtask > macrotask)
      4. Event Loop Monitor: Create a tool that detects and logs operations blocking the event loop > 50ms
      5. Web Worker Calculator: Build a calculator that offloads factorial computation to a Web Worker
      6. Microtask vs Macrotask: Create examples demonstrating when to use Promise.resolve() vs setTimeout(fn, 0)
      Key Takeaways:
      • JavaScript is single-threaded but non-blocking through the event loop
      • Call stack executes synchronous code in LIFO order
      • Microtasks (promises) have higher priority than macrotasks (setTimeout)
      • Microtasks run to completion before next macrotask - can starve macrotasks
      • Long-running sync code blocks the entire thread - break into chunks
      • Use Web Workers for CPU-intensive tasks to keep UI responsive
      • Execution order: Sync → Microtasks → Macrotask → Microtasks → next Macrotask
      • Understanding event loop is essential for debugging async behavior
      What's Next? Continue your learning journey:

      Timers & Async Patterns

      Master timing functions and asynchronous execution patterns

      Introduction: Timing functions are essential for controlling when code executes in JavaScript. From simple delays to complex debouncing and throttling patterns, mastering timers enables you to build responsive, performant applications. Combined with async patterns, timers help manage task scheduling, animations, and user interactions effectively.

      setTimeout and setInterval Basics

      JavaScript provides timer functions to delay execution or repeat tasks at intervals.

      Timer Fundamentals
      // setTimeout - execute once after delay
      setTimeout(() => {
        console.log('Executed after 2 seconds');
      }, 2000);
      
      // setTimeout with parameters
      setTimeout((name, age) => {
        console.log(`${name} is ${age} years old`);
      }, 1000, 'John', 30);
      
      // setInterval - execute repeatedly
      const intervalId = setInterval(() => {
        console.log('This runs every second');
      }, 1000);
      
      // Clear interval after 5 seconds
      setTimeout(() => {
        clearInterval(intervalId);
        console.log('Interval cleared');
      }, 5000);
      
      // Return timeout ID for clearing
      const timeoutId = setTimeout(() => {
        console.log('This might not run');
      }, 3000);
      
      // Clear before it executes
      clearTimeout(timeoutId);
      
      // Countdown timer example
      let count = 10;
      const countdown = setInterval(() => {
        console.log(count);
        count--;
      
        if (count < 0) {
          clearInterval(countdown);
          console.log('Blast off!');
        }
      }, 1000);

      Debounce Implementation

      Debounce delays function execution until after a pause in events, perfect for search inputs and resize handlers.

      Debounce Pattern
      // Basic debounce
      function debounce(func, delay) {
        let timeoutId;
      
        return function(...args) {
          clearTimeout(timeoutId);
      
          timeoutId = setTimeout(() => {
            func.apply(this, args);
          }, delay);
        };
      }
      
      // Usage: Search as user types
      const searchAPI = (query) => {
        console.log('Searching for:', query);
        // Make API call here
      };
      
      const debouncedSearch = debounce(searchAPI, 500);
      
      // Only searches 500ms after user stops typing
      document.querySelector('#search').addEventListener('input', (e) => {
        debouncedSearch(e.target.value);
      });
      
      // Advanced debounce with immediate option
      function debounceAdvanced(func, delay, immediate = false) {
        let timeoutId;
      
        return function(...args) {
          const callNow = immediate && !timeoutId;
      
          clearTimeout(timeoutId);
      
          timeoutId = setTimeout(() => {
            timeoutId = null;
            if (!immediate) {
              func.apply(this, args);
            }
          }, delay);
      
          if (callNow) {
            func.apply(this, args);
          }
        };
      }
      
      // Execute immediately, then debounce subsequent calls
      const saveData = debounceAdvanced((data) => {
        console.log('Saving:', data);
      }, 1000, true);
      
      // Real-world: Window resize handler
      const handleResize = debounce(() => {
        console.log('Window resized to:', window.innerWidth);
        // Recalculate layouts, update responsive components
      }, 250);
      
      window.addEventListener('resize', handleResize);

      Throttle Implementation

      Throttle limits function execution to once per time period, ideal for scroll events and continuous actions.

      Throttle Pattern
      // Basic throttle
      function throttle(func, limit) {
        let inThrottle;
      
        return function(...args) {
          if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
      
            setTimeout(() => {
              inThrottle = false;
            }, limit);
          }
        };
      }
      
      // Usage: Scroll event handler
      const handleScroll = () => {
        console.log('Scroll position:', window.scrollY);
        // Update scroll-based animations, lazy load images
      };
      
      const throttledScroll = throttle(handleScroll, 200);
      window.addEventListener('scroll', throttledScroll);
      
      // Advanced throttle with trailing call
      function throttleAdvanced(func, limit) {
        let inThrottle;
        let lastFunc;
        let lastRan;
      
        return function(...args) {
          if (!inThrottle) {
            func.apply(this, args);
            lastRan = Date.now();
            inThrottle = true;
      
            setTimeout(() => {
              inThrottle = false;
      
              if (lastFunc) {
                throttleAdvanced(func, limit).apply(this, args);
                lastFunc = null;
              }
            }, limit);
          } else {
            lastFunc = args;
          }
        };
      }
      
      // Real-world: Button click prevention
      const submitForm = throttle((formData) => {
        console.log('Submitting form...');
        // Prevent multiple rapid submissions
      }, 2000);
      
      document.querySelector('#submitBtn').addEventListener('click', () => {
        submitForm({ name: 'John', email: 'john@example.com' });
      });
      
      // Comparison: Debounce vs Throttle
      const input = document.querySelector('#input');
      
      // Debounce: Wait for pause
      input.addEventListener('input', debounce((e) => {
        console.log('Debounced:', e.target.value);
      }, 500));
      
      // Throttle: Execute at regular intervals
      input.addEventListener('input', throttle((e) => {
        console.log('Throttled:', e.target.value);
      }, 500));

      requestAnimationFrame

      RAF synchronizes with browser repaints for smooth animations at 60fps, more efficient than setInterval.

      Animation Frame API
      // Basic animation loop
      function animate() {
        // Update animation state
        console.log('Frame rendered');
      
        requestAnimationFrame(animate);
      }
      
      animate();
      
      // Controlled animation with start/stop
      class Animation {
        constructor(callback) {
          this.callback = callback;
          this.rafId = null;
          this.running = false;
        }
      
        start() {
          if (this.running) return;
          this.running = true;
      
          const loop = (timestamp) => {
            this.callback(timestamp);
      
            if (this.running) {
              this.rafId = requestAnimationFrame(loop);
            }
          };
      
          this.rafId = requestAnimationFrame(loop);
        }
      
        stop() {
          this.running = false;
          if (this.rafId) {
            cancelAnimationFrame(this.rafId);
          }
        }
      }
      
      // Usage: Smooth counter animation
      const counter = document.querySelector('#counter');
      let count = 0;
      const target = 1000;
      const duration = 2000;
      let startTime = null;
      
      function animateCounter(timestamp) {
        if (!startTime) startTime = timestamp;
        const progress = timestamp - startTime;
      
        const percentage = Math.min(progress / duration, 1);
        count = Math.floor(percentage * target);
      
        counter.textContent = count;
      
        if (percentage < 1) {
          requestAnimationFrame(animateCounter);
        }
      }
      
      requestAnimationFrame(animateCounter);
      
      // Smooth scroll implementation
      function smoothScrollTo(targetY, duration = 1000) {
        const startY = window.scrollY;
        const distance = targetY - startY;
        let startTime = null;
      
        function scroll(currentTime) {
          if (!startTime) startTime = currentTime;
          const elapsed = currentTime - startTime;
          const progress = Math.min(elapsed / duration, 1);
      
          // Easing function
          const ease = progress * (2 - progress); // easeOutQuad
      
          window.scrollTo(0, startY + distance * ease);
      
          if (progress < 1) {
            requestAnimationFrame(scroll);
          }
        }
      
        requestAnimationFrame(scroll);
      }
      
      // Usage
      document.querySelector('#scrollBtn').addEventListener('click', () => {
        smoothScrollTo(1000, 800);
      });

      Async Task Queuing

      Queue async tasks to control concurrency and ensure orderly execution.

      Task Queue Implementation
      // Simple task queue
      class TaskQueue {
        constructor() {
          this.queue = [];
          this.running = false;
        }
      
        async add(task) {
          return new Promise((resolve, reject) => {
            this.queue.push(async () => {
              try {
                const result = await task();
                resolve(result);
              } catch (error) {
                reject(error);
              }
            });
      
            this.process();
          });
        }
      
        async process() {
          if (this.running || this.queue.length === 0) return;
      
          this.running = true;
          const task = this.queue.shift();
      
          await task();
      
          this.running = false;
          this.process();
        }
      }
      
      // Usage
      const queue = new TaskQueue();
      
      queue.add(() => fetch('/api/user/1').then(r => r.json()));
      queue.add(() => fetch('/api/user/2').then(r => r.json()));
      queue.add(() => fetch('/api/user/3').then(r => r.json()));
      
      // Concurrent task queue (limit parallel execution)
      class ConcurrentQueue {
        constructor(maxConcurrent = 2) {
          this.maxConcurrent = maxConcurrent;
          this.running = 0;
          this.queue = [];
        }
      
        async add(task) {
          return new Promise((resolve, reject) => {
            this.queue.push({ task, resolve, reject });
            this.process();
          });
        }
      
        async process() {
          while (this.running < this.maxConcurrent && this.queue.length > 0) {
            const { task, resolve, reject } = this.queue.shift();
            this.running++;
      
            try {
              const result = await task();
              resolve(result);
            } catch (error) {
              reject(error);
            } finally {
              this.running--;
              this.process();
            }
          }
        }
      }
      
      // Usage: Limit to 3 concurrent API calls
      const apiQueue = new ConcurrentQueue(3);
      
      const promises = Array.from({ length: 10 }, (_, i) =>
        apiQueue.add(() => fetch(`/api/item/${i}`).then(r => r.json()))
      );
      
      const results = await Promise.all(promises);

      Task Scheduling Patterns

      Schedule tasks to run at specific times or intervals with advanced control.

      Advanced Scheduling
      // Delayed execution with promise
      function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
      }
      
      // Usage with async/await
      async function sequence() {
        console.log('Start');
        await delay(1000);
        console.log('After 1 second');
        await delay(2000);
        console.log('After 3 seconds total');
      }
      
      // Retry with delay
      async function retryWithDelay(fn, retries = 3, delayMs = 1000) {
        for (let i = 0; i < retries; i++) {
          try {
            return await fn();
          } catch (error) {
            if (i === retries - 1) throw error;
            console.log(`Retry ${i + 1} after ${delayMs}ms`);
            await delay(delayMs);
          }
        }
      }
      
      // Timeout promise
      function timeout(promise, ms) {
        return Promise.race([
          promise,
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Timeout')), ms)
          )
        ]);
      }
      
      // Usage
      try {
        const data = await timeout(
          fetch('https://api.example.com/slow'),
          5000
        );
      } catch (error) {
        console.error('Request timed out');
      }
      
      // Scheduled task runner
      class Scheduler {
        constructor() {
          this.tasks = [];
        }
      
        schedule(task, delay) {
          const id = setTimeout(() => {
            task();
            this.tasks = this.tasks.filter(t => t.id !== id);
          }, delay);
      
          this.tasks.push({ id, task, delay });
          return id;
        }
      
        repeat(task, interval) {
          const id = setInterval(task, interval);
          this.tasks.push({ id, task, interval, repeating: true });
          return id;
        }
      
        cancel(id) {
          clearTimeout(id);
          clearInterval(id);
          this.tasks = this.tasks.filter(t => t.id !== id);
        }
      
        cancelAll() {
          this.tasks.forEach(({ id }) => {
            clearTimeout(id);
            clearInterval(id);
          });
          this.tasks = [];
        }
      }
      
      // Usage
      const scheduler = new Scheduler();
      
      scheduler.schedule(() => console.log('One time task'), 1000);
      const repeatId = scheduler.repeat(() => console.log('Repeating'), 2000);
      
      // Cancel after 10 seconds
      setTimeout(() => scheduler.cancel(repeatId), 10000);

      Performance Timing

      Measure execution time and optimize performance-critical code paths.

      Performance Measurement
      // Basic timing with console.time
      console.time('operation');
      // ... code to measure
      console.timeEnd('operation'); // Logs: operation: 123.456ms
      
      // Performance API
      const start = performance.now();
      // ... code to measure
      const end = performance.now();
      console.log(`Execution took ${end - start}ms`);
      
      // Function timing wrapper
      function measureTime(fn, label = 'Function') {
        return function(...args) {
          const start = performance.now();
          const result = fn.apply(this, args);
          const end = performance.now();
          console.log(`${label} took ${end - start}ms`);
          return result;
        };
      }
      
      // Async function timing
      async function measureAsync(fn, label = 'Async Function') {
        const start = performance.now();
        try {
          const result = await fn();
          const end = performance.now();
          console.log(`${label} took ${end - start}ms`);
          return result;
        } catch (error) {
          const end = performance.now();
          console.log(`${label} failed after ${end - start}ms`);
          throw error;
        }
      }
      
      // Performance marks and measures
      performance.mark('start-fetch');
      
      await fetch('https://api.example.com/data');
      
      performance.mark('end-fetch');
      performance.measure('fetch-duration', 'start-fetch', 'end-fetch');
      
      const measures = performance.getEntriesByType('measure');
      console.log(measures[0].duration);
      
      // Benchmark utility
      class Benchmark {
        constructor(iterations = 1000) {
          this.iterations = iterations;
        }
      
        run(fn, name = 'Test') {
          const times = [];
      
          for (let i = 0; i < this.iterations; i++) {
            const start = performance.now();
            fn();
            const end = performance.now();
            times.push(end - start);
          }
      
          const avg = times.reduce((a, b) => a + b) / times.length;
          const min = Math.min(...times);
          const max = Math.max(...times);
      
          console.log(`${name} - Avg: ${avg.toFixed(3)}ms, Min: ${min.toFixed(3)}ms, Max: ${max.toFixed(3)}ms`);
        }
      }
      
      // Usage
      const bench = new Benchmark(10000);
      bench.run(() => Array.from({ length: 100 }, (_, i) => i * 2), 'Array.from');
      bench.run(() => [...Array(100)].map((_, i) => i * 2), 'Spread operator');

      Real-World Patterns

      Combine timing patterns for production-ready solutions.

      Combined Patterns
      // Auto-save with debounce
      class AutoSave {
        constructor(saveFn, delay = 2000) {
          this.saveFn = saveFn;
          this.delay = delay;
          this.saveDebounced = debounce(this.save.bind(this), delay);
          this.isDirty = false;
        }
      
        onChange(data) {
          this.isDirty = true;
          this.saveDebounced(data);
        }
      
        async save(data) {
          if (!this.isDirty) return;
      
          try {
            await this.saveFn(data);
            this.isDirty = false;
            console.log('Saved successfully');
          } catch (error) {
            console.error('Save failed:', error);
          }
        }
      }
      
      // Usage
      const autoSave = new AutoSave(
        (data) => fetch('/api/save', {
          method: 'POST',
          body: JSON.stringify(data)
        })
      );
      
      document.querySelector('#editor').addEventListener('input', (e) => {
        autoSave.onChange({ content: e.target.value });
      });
      
      // Infinite scroll with throttle
      class InfiniteScroll {
        constructor(loadMoreFn) {
          this.loadMoreFn = loadMoreFn;
          this.loading = false;
          this.hasMore = true;
      
          this.handleScroll = throttle(this.checkScroll.bind(this), 200);
          window.addEventListener('scroll', this.handleScroll);
        }
      
        checkScroll() {
          if (this.loading || !this.hasMore) return;
      
          const scrollPosition = window.scrollY + window.innerHeight;
          const threshold = document.documentElement.scrollHeight - 200;
      
          if (scrollPosition >= threshold) {
            this.loadMore();
          }
        }
      
        async loadMore() {
          this.loading = true;
      
          try {
            const items = await this.loadMoreFn();
            this.hasMore = items.length > 0;
          } catch (error) {
            console.error('Failed to load more:', error);
          } finally {
            this.loading = false;
          }
        }
      
        destroy() {
          window.removeEventListener('scroll', this.handleScroll);
        }
      }
      
      // Usage
      const infiniteScroll = new InfiniteScroll(async () => {
        const response = await fetch('/api/items?page=' + currentPage);
        const items = await response.json();
        currentPage++;
        return items;
      });

      Practice Exercises

      1. Debounced Search: Create a search input that debounces API calls by 500ms and shows loading state
      2. Throttled Scroll Progress: Build a reading progress bar that updates on scroll (throttled to 100ms)
      3. Animated Counter: Implement a counter that animates from 0 to a target number using requestAnimationFrame
      4. Task Queue: Build a task queue that limits concurrent API requests to 3 at a time
      5. Auto-Save Feature: Create an auto-save system that saves changes 2 seconds after user stops typing
      6. Performance Monitor: Build a utility that measures and logs execution time of async functions
      Key Takeaways:
      • Use setTimeout for single delayed execution, setInterval for repeated tasks
      • Debounce delays execution until activity stops - perfect for search inputs
      • Throttle limits execution frequency - ideal for scroll/resize handlers
      • requestAnimationFrame is superior to setInterval for animations (syncs with browser repaints)
      • Always clear timers (clearTimeout/clearInterval) to prevent memory leaks
      • Task queues control concurrency and prevent overwhelming servers
      • Performance API provides high-precision timing measurements
      • Combine patterns (debounce + queue) for robust real-world solutions
      What's Next? Continue your learning journey:

      Promises & Async/Await

      Coordinate asynchronous work with promises, chaining, and async functions.

      Promises represent future values. They transition from pending to fulfilled or rejected. Combine .then/.catch for chaining, use utilities like Promise.all, and switch to async/await for linear-style code while still returning promises.

      Creating Promises

      Wrap asynchronous work in the Promise constructor or return promises from APIs.

      Manual Promise
      function delay(ms) {
        return new Promise((resolve) => {
          setTimeout(() => resolve(`Done in ${ms}ms`), ms);
        });
      }
      
      delay(500).then(console.log);
      Rejecting
      function fetchUser(id) {
        return new Promise((resolve, reject) => {
          if (!id) return reject(new Error('Missing id'));
          resolve({ id, name: 'Nova' });
        });
      }
      
      fetchUser(null).catch(err => console.error(err.message));

      Chaining then/catch/finally

      Return values propagate through chains; returning a promise waits for it.

      Chain Example
      delay(200)
        .then(msg => {
          console.log(msg);
          return fetchUser(1);
        })
        .then(user => console.log(user.name))
        .catch(err => console.error('Error', err))
        .finally(() => console.log('Done'));

      Promise Utilities

      Run tasks concurrently or race them using built-in combinators.

      Promise.all
      const a = delay(100);
      const b = delay(200);
      const c = delay(300);
      
      Promise.all([a, b, c])
        .then(results => console.log(results))
        .catch(err => console.error(err));
      race vs allSettled
      Promise.race([delay(1000), delay(50)]).then(console.log);
      
      Promise.allSettled([
        Promise.resolve('ok'),
        Promise.reject('fail')
      ]).then(console.log);

      Async/Await Syntax

      async functions return promises. await pauses until fulfillment inside an async function.

      Linear Style
      async function loadProfile() {
        try {
          const user = await fetchUser(2);
          const message = await delay(100);
          return { user, message };
        } catch (err) {
          console.error(err);
          return null;
        }
      }
      
      loadProfile().then(console.log);

      Error Handling

      Use .catch or try/catch within async functions to handle rejections.

      Retry Wrapper
      async function withRetry(fn, attempts = 3) {
        let lastError;
        for (let i = 0; i < attempts; i++) {
          try {
            return await fn();
          } catch (err) {
            lastError = err;
          }
        }
        throw lastError;
      }
      
      withRetry(() => fetchUser(null)).catch(err => console.error('Failed', err.message));

      Parallel vs Sequential

      Kick off promises before awaiting to run tasks in parallel.

      Parallel Fetch
      async function loadDashboard() {
        const userPromise = fetchUser(3);
        const dataPromise = delay(120);
      
        const [user, message] = await Promise.all([userPromise, dataPromise]);
        return { user, message };
      }
      
      loadDashboard().then(console.log);

      Avoiding Callback Hell

      Promises and async/await flatten nested callbacks, improving readability.

      From Callbacks to Promises
      // Callback style
      // fetch(url, res => {
      //   parse(res, parsed => {
      //     save(parsed, () => console.log('done'));
      //   });
      // });
      
      // Promise style
      fetch(url)
        .then(parse)
        .then(save)
        .then(() => console.log('done'));
      
      // Async style
      async function run() {
        const res = await fetch(url);
        const parsed = await parse(res);
        await save(parsed);
        console.log('done');
      }

      Microtasks and Event Loop

      Promise callbacks run as microtasks after the current call stack, before timers.

      Execution Order
      console.log('start');
      
      Promise.resolve('p').then(console.log);
      setTimeout(() => console.log('timeout'), 0);
      
      console.log('end');
      // order: start, end, p, timeout
      + +

      Cancellation and Timeouts

      +

      Promises themselves cannot be canceled, but you can design APIs that support aborting.

      +
      +
      AbortController with fetch
      +
      const controller = new AbortController();
      +const { signal } = controller;
      +
      +fetch('/api/data', { signal })
      +  .then(res => res.json())
      +  .then(console.log)
      +  .catch(err => {
      +    if (err.name === 'AbortError') console.log('Request canceled');
      +  });
      +
      +setTimeout(() => controller.abort(), 50);
      +
      + +

      Handling Unhandled Rejections

      +

      Always attach .catch or wrap awaits in try/catch to prevent unhandled rejection warnings.

      +
      +
      Global Handling
      +
      window.addEventListener('unhandledrejection', (event) => {
      +  console.error('Unhandled', event.reason);
      +});
      +
      +Promise.reject(new Error('boom')); // caught by listener
      +

      Practice Exercises

      1. Wrap setTimeout in a promise and await it to simulate delays.
      2. Chain two asynchronous operations and handle errors with a single .catch.
      3. Use Promise.all to fetch three resources simultaneously and handle a rejection.
      4. Rewrite a callback-based function to return a promise.
      5. Create an async function that runs two promises in parallel and another sequentially; log timing differences.
      6. Implement a retry helper that stops after n failed attempts.
      7. Compare Promise.race and Promise.allSettled on mixed success/failure promises.
      8. Use finally to clean up UI state after a promise chain.
      9. Demonstrate microtask ordering by mixing Promise.resolve with setTimeout.
      10. Write a small utility that times how long an async function takes to resolve.
      Key Takeaways:
      • Promises model eventual values and move through pending, fulfilled, or rejected states.
      • Chain .then/.catch/.finally to compose async flows; return promises to control sequencing.
      • Promise.all awaits all, Promise.race resolves on the first, and Promise.allSettled collects results regardless of outcome.
      • async/await offers synchronous-looking code while still leveraging promises and microtasks.
      • Error handling remains explicit; always catch and surface rejections to avoid silent failures.

      What's Next?

      Explore Fetch and APIs to apply promises to real network calls, or revisit Classes for structuring asynchronous service layers.

      Fetch API & HTTP Requests

      Master modern HTTP communication with the Fetch API

      Introduction: The Fetch API provides a modern, promise-based interface for making HTTP requests in JavaScript. It replaces XMLHttpRequest with a cleaner, more powerful API that handles requests and responses with ease. Understanding Fetch is essential for building dynamic web applications that communicate with servers and external APIs.

      Fetch API Basics

      The fetch() function returns a promise that resolves to a Response object. Basic syntax requires just a URL.

      Basic Fetch Request
      // Simple GET request
      fetch('https://api.example.com/data')
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(error => console.error('Error:', error));
      
      // Using async/await (preferred)
      async function fetchData() {
        try {
          const response = await fetch('https://api.example.com/data');
          const data = await response.json();
          console.log(data);
        } catch (error) {
          console.error('Error:', error);
        }
      }
      
      // Check response status
      async function fetchWithStatusCheck() {
        const response = await fetch('https://api.example.com/data');
      
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
      
        return await response.json();
      }

      GET and POST Requests

      GET retrieves data, while POST sends data to the server. Configure requests using the options object.

      GET and POST Examples
      // GET request with query parameters
      async function getUsers(page = 1) {
        const url = new URL('https://api.example.com/users');
        url.searchParams.append('page', page);
        url.searchParams.append('limit', 10);
      
        const response = await fetch(url);
        return await response.json();
      }
      
      // POST request with JSON body
      async function createUser(userData) {
        const response = await fetch('https://api.example.com/users', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(userData)
        });
      
        if (!response.ok) {
          throw new Error(`Failed to create user: ${response.status}`);
        }
      
        return await response.json();
      }
      
      // PUT request to update data
      async function updateUser(id, updates) {
        const response = await fetch(`https://api.example.com/users/${id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(updates)
        });
        return await response.json();
      }
      
      // DELETE request
      async function deleteUser(id) {
        const response = await fetch(`https://api.example.com/users/${id}`, {
          method: 'DELETE'
        });
        return response.ok;
      }

      Request Headers and Body

      Headers provide metadata about requests. Body formats include JSON, FormData, and plain text.

      Headers and Different Body Types
      // Custom headers
      async function fetchWithAuth(token) {
        const response = await fetch('https://api.example.com/protected', {
          headers: {
            'Authorization': `Bearer ${token}`,
            'Accept': 'application/json',
            'X-Custom-Header': 'value'
          }
        });
        return await response.json();
      }
      
      // FormData for file uploads
      async function uploadFile(file) {
        const formData = new FormData();
        formData.append('file', file);
        formData.append('description', 'My file');
      
        const response = await fetch('https://api.example.com/upload', {
          method: 'POST',
          body: formData // Don't set Content-Type, browser sets it
        });
        return await response.json();
      }
      
      // Multiple files with FormData
      async function uploadMultipleFiles(files) {
        const formData = new FormData();
      
        for (let i = 0; i < files.length; i++) {
          formData.append('files[]', files[i]);
        }
      
        const response = await fetch('https://api.example.com/upload-multiple', {
          method: 'POST',
          body: formData
        });
        return await response.json();
      }
      
      // URLSearchParams for form-encoded data
      async function submitForm(data) {
        const params = new URLSearchParams();
        params.append('username', data.username);
        params.append('password', data.password);
      
        const response = await fetch('https://api.example.com/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: params
        });
        return await response.json();
      }

      Response Handling and Status Codes

      Handle different response types and HTTP status codes appropriately for robust error handling.

      Response Types and Status Handling
      // Different response types
      async function handleDifferentResponses(url) {
        const response = await fetch(url);
      
        const contentType = response.headers.get('content-type');
      
        if (contentType.includes('application/json')) {
          return await response.json();
        } else if (contentType.includes('text/html')) {
          return await response.text();
        } else if (contentType.includes('image')) {
          return await response.blob();
        } else {
          return await response.arrayBuffer();
        }
      }
      
      // Handle specific status codes
      async function fetchWithStatusHandling(url) {
        const response = await fetch(url);
      
        switch (response.status) {
          case 200:
            return await response.json();
          case 201:
            console.log('Resource created successfully');
            return await response.json();
          case 204:
            return null; // No content
          case 400:
            throw new Error('Bad request - check your data');
          case 401:
            throw new Error('Unauthorized - login required');
          case 403:
            throw new Error('Forbidden - insufficient permissions');
          case 404:
            throw new Error('Resource not found');
          case 500:
            throw new Error('Server error - try again later');
          default:
            throw new Error(`Unexpected status: ${response.status}`);
        }
      }
      
      // Parse error responses
      async function handleErrorResponse(response) {
        if (!response.ok) {
          let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
      
          try {
            const errorData = await response.json();
            errorMessage = errorData.message || errorMessage;
          } catch (e) {
            // Response wasn't JSON
          }
      
          throw new Error(errorMessage);
        }
        return response;
      }

      Error Handling and Retry Logic

      Implement robust error handling with retry mechanisms for network failures and timeouts.

      Advanced Error Handling
      // Retry logic with exponential backoff
      async function fetchWithRetry(url, options = {}, maxRetries = 3) {
        let lastError;
      
        for (let i = 0; i < maxRetries; i++) {
          try {
            const response = await fetch(url, options);
      
            if (!response.ok) {
              throw new Error(`HTTP ${response.status}`);
            }
      
            return await response.json();
          } catch (error) {
            lastError = error;
            console.log(`Attempt ${i + 1} failed:`, error.message);
      
            if (i < maxRetries - 1) {
              // Exponential backoff: 1s, 2s, 4s
              const delay = Math.pow(2, i) * 1000;
              await new Promise(resolve => setTimeout(resolve, delay));
            }
          }
        }
      
        throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
      }
      
      // Timeout wrapper
      async function fetchWithTimeout(url, options = {}, timeout = 5000) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);
      
        try {
          const response = await fetch(url, {
            ...options,
            signal: controller.signal
          });
          clearTimeout(timeoutId);
          return response;
        } catch (error) {
          clearTimeout(timeoutId);
          if (error.name === 'AbortError') {
            throw new Error('Request timeout');
          }
          throw error;
        }
      }
      
      // Combined: retry with timeout
      async function robustFetch(url, options = {}) {
        return fetchWithRetry(
          url,
          options,
          3
        ).catch(error => {
          console.error('All retry attempts failed:', error);
          throw error;
        });
      }

      AbortController and Request Cancellation

      Use AbortController to cancel ongoing requests, preventing memory leaks and unnecessary network usage.

      Request Cancellation
      // Basic abort
      const controller = new AbortController();
      
      fetch('https://api.example.com/data', {
        signal: controller.signal
      })
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(error => {
          if (error.name === 'AbortError') {
            console.log('Request was cancelled');
          }
        });
      
      // Cancel after 5 seconds
      setTimeout(() => controller.abort(), 5000);
      
      // Cancellable search with debounce
      class SearchManager {
        constructor() {
          this.controller = null;
        }
      
        async search(query) {
          // Cancel previous request
          if (this.controller) {
            this.controller.abort();
          }
      
          this.controller = new AbortController();
      
          try {
            const response = await fetch(
              `https://api.example.com/search?q=${query}`,
              { signal: this.controller.signal }
            );
            return await response.json();
          } catch (error) {
            if (error.name === 'AbortError') {
              console.log('Search cancelled');
              return null;
            }
            throw error;
          }
        }
      }
      
      const searchManager = new SearchManager();
      
      // Use in search input
      document.querySelector('#searchInput').addEventListener('input', (e) => {
        searchManager.search(e.target.value)
          .then(results => {
            if (results) {
              displayResults(results);
            }
          });
      });

      CORS and Authentication

      Handle Cross-Origin Resource Sharing and implement various authentication methods.

      CORS and Auth Patterns
      // CORS with credentials
      async function fetchWithCredentials(url) {
        const response = await fetch(url, {
          credentials: 'include', // Send cookies
          mode: 'cors' // Explicit CORS mode
        });
        return await response.json();
      }
      
      // Bearer token authentication
      class ApiClient {
        constructor(baseURL, token) {
          this.baseURL = baseURL;
          this.token = token;
        }
      
        async request(endpoint, options = {}) {
          const url = `${this.baseURL}${endpoint}`;
          const headers = {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${this.token}`,
            ...options.headers
          };
      
          const response = await fetch(url, {
            ...options,
            headers
          });
      
          if (response.status === 401) {
            // Token expired, refresh or redirect to login
            throw new Error('Authentication required');
          }
      
          return await response.json();
        }
      
        get(endpoint) {
          return this.request(endpoint);
        }
      
        post(endpoint, data) {
          return this.request(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
          });
        }
      }
      
      // Usage
      const api = new ApiClient('https://api.example.com', 'your-token-here');
      const users = await api.get('/users');
      const newUser = await api.post('/users', { name: 'John' });
      
      // API key authentication
      async function fetchWithApiKey(url, apiKey) {
        const response = await fetch(url, {
          headers: {
            'X-API-Key': apiKey
          }
        });
        return await response.json();
      }

      Real-World Patterns

      Common patterns for production applications including caching, batching, and request queuing.

      Production Patterns
      // Simple cache wrapper
      class CachedFetch {
        constructor(ttl = 60000) { // 1 minute default
          this.cache = new Map();
          this.ttl = ttl;
        }
      
        async fetch(url, options = {}) {
          const cacheKey = url + JSON.stringify(options);
          const cached = this.cache.get(cacheKey);
      
          if (cached && Date.now() - cached.timestamp < this.ttl) {
            return cached.data;
          }
      
          const response = await fetch(url, options);
          const data = await response.json();
      
          this.cache.set(cacheKey, {
            data,
            timestamp: Date.now()
          });
      
          return data;
        }
      
        clear() {
          this.cache.clear();
        }
      }
      
      // Batch requests
      class RequestBatcher {
        constructor(batchFn, delay = 50) {
          this.batchFn = batchFn;
          this.delay = delay;
          this.queue = [];
          this.timeoutId = null;
        }
      
        request(id) {
          return new Promise((resolve, reject) => {
            this.queue.push({ id, resolve, reject });
      
            if (this.timeoutId) {
              clearTimeout(this.timeoutId);
            }
      
            this.timeoutId = setTimeout(() => this.flush(), this.delay);
          });
        }
      
        async flush() {
          const batch = this.queue.splice(0);
          const ids = batch.map(item => item.id);
      
          try {
            const results = await this.batchFn(ids);
            batch.forEach((item, index) => {
              item.resolve(results[index]);
            });
          } catch (error) {
            batch.forEach(item => item.reject(error));
          }
        }
      }
      
      // Usage
      const userBatcher = new RequestBatcher(async (ids) => {
        const response = await fetch('/api/users/batch', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ids })
        });
        return await response.json();
      });
      
      // These get batched together
      const user1 = await userBatcher.request(1);
      const user2 = await userBatcher.request(2);
      const user3 = await userBatcher.request(3);

      Practice Exercises

      1. User API Client: Build a complete API client class with GET, POST, PUT, DELETE methods, error handling, and authentication
      2. Search with Debounce: Implement a search feature that fetches results as user types, with debouncing and request cancellation
      3. File Upload with Progress: Create a file upload component using FormData with upload progress indication
      4. Retry Logic: Implement a fetch wrapper with exponential backoff retry logic and configurable retry count
      5. Request Queue: Build a request queue that limits concurrent requests to 3 at a time
      6. Cached API: Create a caching layer for API requests with TTL and cache invalidation
      Key Takeaways:
      • Fetch returns promises - always handle both success and error cases
      • Check response.ok before parsing - fetch doesn't reject on HTTP errors
      • Use AbortController to cancel requests and prevent memory leaks
      • Set appropriate headers, especially Content-Type for POST/PUT requests
      • Implement retry logic with exponential backoff for network failures
      • Handle different response types: json(), text(), blob(), arrayBuffer()
      • Use timeout wrappers to prevent hanging requests
      • Consider caching and batching for performance optimization
      What's Next? Continue your learning journey:

      Error Handling & Debugging

      Master error management and debugging techniques for robust applications

      Introduction: Robust error handling and effective debugging are crucial skills for professional JavaScript development. Errors are inevitable, but how you handle them determines application reliability and user experience. From try/catch blocks to browser DevTools, mastering these techniques enables you to build resilient applications, diagnose issues quickly, and provide meaningful feedback when things go wrong.

      Try/Catch/Finally

      Handle errors gracefully with try/catch blocks to prevent application crashes.

      Basic Error Handling
      // Basic try/catch
      try {
        const result = riskyOperation();
        console.log(result);
      } catch (error) {
        console.error('Error occurred:', error);
      }
      
      // Finally block - always executes
      try {
        connectToDatabase();
        performQuery();
      } catch (error) {
        console.error('Database error:', error);
      } finally {
        disconnectFromDatabase(); // Always runs
      }
      
      // Catch error details
      try {
        JSON.parse('invalid json');
      } catch (error) {
        console.log('Name:', error.name); // 'SyntaxError'
        console.log('Message:', error.message); // Description
        console.log('Stack:', error.stack); // Stack trace
      }
      
      // Multiple operations
      try {
        const data = fetchData();
        const parsed = JSON.parse(data);
        const validated = validateData(parsed);
        processData(validated);
      } catch (error) {
        // Single catch handles all errors
        console.error('Pipeline failed:', error);
      }
      
      // Nested try/catch
      try {
        try {
          innerOperation();
        } catch (innerError) {
          console.log('Inner error:', innerError);
          throw innerError; // Re-throw to outer catch
        }
      } catch (outerError) {
        console.log('Outer error:', outerError);
      }
      
      // Try/catch with async code (needs await)
      try {
        const response = await fetch('/api/data');
        const data = await response.json();
      } catch (error) {
        console.error('Fetch failed:', error);
      }
      
      // Try/catch doesn't catch async errors without await
      try {
        fetch('/api/data').then(r => r.json()); // Error not caught!
      } catch (error) {
        // This won't catch fetch errors
      }
      
      // Correct async error handling
      fetch('/api/data')
        .then(r => r.json())
        .catch(error => {
          console.error('Fetch failed:', error);
        });

      Throw Custom Errors

      Create and throw custom errors for better error handling and debugging.

      Throwing Errors
      // Throw built-in Error
      function divide(a, b) {
        if (b === 0) {
          throw new Error('Division by zero');
        }
        return a / b;
      }
      
      try {
        divide(10, 0);
      } catch (error) {
        console.error(error.message); // 'Division by zero'
      }
      
      // Throw any value (not recommended)
      throw 'Simple string error'; // Works but less useful
      throw { message: 'Error object' }; // Works
      throw 42; // Works but confusing
      
      // Throw with error type
      throw new TypeError('Expected a number');
      throw new RangeError('Value out of range');
      throw new ReferenceError('Variable not defined');
      
      // Custom error class
      class ValidationError extends Error {
        constructor(message, field) {
          super(message);
          this.name = 'ValidationError';
          this.field = field;
        }
      }
      
      function validateEmail(email) {
        if (!email) {
          throw new ValidationError('Email is required', 'email');
        }
      
        if (!email.includes('@')) {
          throw new ValidationError('Invalid email format', 'email');
        }
      
        return true;
      }
      
      try {
        validateEmail('invalid');
      } catch (error) {
        if (error instanceof ValidationError) {
          console.log(`Validation failed for ${error.field}: ${error.message}`);
        } else {
          console.error('Unexpected error:', error);
        }
      }
      
      // Multiple custom error types
      class NetworkError extends Error {
        constructor(message, statusCode) {
          super(message);
          this.name = 'NetworkError';
          this.statusCode = statusCode;
        }
      }
      
      class AuthenticationError extends Error {
        constructor(message) {
          super(message);
          this.name = 'AuthenticationError';
        }
      }
      
      async function fetchData(url) {
        const response = await fetch(url);
      
        if (response.status === 401) {
          throw new AuthenticationError('Not authenticated');
        }
      
        if (!response.ok) {
          throw new NetworkError(
            `HTTP error ${response.status}`,
            response.status
          );
        }
      
        return await response.json();
      }
      
      // Handle different error types
      try {
        await fetchData('/api/data');
      } catch (error) {
        if (error instanceof AuthenticationError) {
          redirectToLogin();
        } else if (error instanceof NetworkError) {
          showNetworkError(error.statusCode);
        } else {
          showGenericError();
        }
      }

      Error Types

      JavaScript has several built-in error types for different error conditions.

      Built-in Error Types
      // Error - Generic error
      throw new Error('Something went wrong');
      
      // TypeError - Wrong type
      const num = 42;
      // num.toUpperCase(); // TypeError: num.toUpperCase is not a function
      
      function expectString(str) {
        if (typeof str !== 'string') {
          throw new TypeError('Expected a string');
        }
      }
      
      // RangeError - Number out of range
      function setAge(age) {
        if (age < 0 || age > 150) {
          throw new RangeError('Age must be between 0 and 150');
        }
      }
      
      // ReferenceError - Variable not found
      try {
        console.log(undefinedVariable); // ReferenceError
      } catch (error) {
        console.log(error.name); // 'ReferenceError'
      }
      
      // SyntaxError - Invalid syntax
      try {
        eval('{ invalid syntax }'); // SyntaxError
      } catch (error) {
        console.log(error.name); // 'SyntaxError'
      }
      
      // URIError - Invalid URI encoding
      try {
        decodeURIComponent('%'); // URIError
      } catch (error) {
        console.log(error.name); // 'URIError'
      }
      
      // EvalError - Error in eval() (rarely used)
      // Mostly deprecated in modern JavaScript
      
      // Catching specific error types
      try {
        riskyOperation();
      } catch (error) {
        if (error instanceof TypeError) {
          console.log('Type error:', error.message);
        } else if (error instanceof RangeError) {
          console.log('Range error:', error.message);
        } else if (error instanceof ReferenceError) {
          console.log('Reference error:', error.message);
        } else {
          console.log('Unknown error:', error);
        }
      }
      
      // Check error by name
      try {
        riskyOperation();
      } catch (error) {
        switch (error.name) {
          case 'TypeError':
            handleTypeError(error);
            break;
          case 'RangeError':
            handleRangeError(error);
            break;
          default:
            handleGenericError(error);
        }
      }

      Stack Traces

      Stack traces show the call stack when an error occurred, essential for debugging.

      Reading Stack Traces
      // Generate stack trace
      function level3() {
        throw new Error('Error at level 3');
      }
      
      function level2() {
        level3();
      }
      
      function level1() {
        level2();
      }
      
      try {
        level1();
      } catch (error) {
        console.log(error.stack);
        /*
        Error: Error at level 3
            at level3 (script.js:2:9)
            at level2 (script.js:6:3)
            at level1 (script.js:10:3)
            at :1:1
        */
      }
      
      // Get stack trace without error
      function getStackTrace() {
        const stack = new Error().stack;
        return stack;
      }
      
      console.log(getStackTrace());
      
      // Custom stack trace
      class CustomError extends Error {
        constructor(message, details) {
          super(message);
          this.name = 'CustomError';
          this.details = details;
      
          // Maintain proper stack trace
          if (Error.captureStackTrace) {
            Error.captureStackTrace(this, CustomError);
          }
        }
      }
      
      // Parse stack trace
      function parseStackTrace(error) {
        const lines = error.stack.split('\n');
        const frames = [];
      
        for (const line of lines) {
          const match = line.match(/at (.+) \((.+):(\d+):(\d+)\)/);
          if (match) {
            frames.push({
              function: match[1],
              file: match[2],
              line: parseInt(match[3]),
              column: parseInt(match[4])
            });
          }
        }
      
        return frames;
      }
      
      try {
        level1();
      } catch (error) {
        const frames = parseStackTrace(error);
        console.log('Call stack:', frames);
      }

      Console Methods

      Browser console provides powerful debugging methods beyond console.log.

      Console API
      // Basic logging
      console.log('Regular message');
      console.info('Info message');
      console.warn('Warning message');
      console.error('Error message');
      
      // Multiple arguments
      console.log('User:', user, 'Count:', count);
      
      // String formatting
      console.log('Hello %s', 'World');
      console.log('Number: %d', 42);
      console.log('Object: %o', { name: 'John' });
      
      // CSS styling
      console.log('%cStyled text', 'color: blue; font-size: 20px; font-weight: bold');
      console.log(
        '%cError: %cSomething failed',
        'color: red; font-weight: bold',
        'color: black'
      );
      
      // Group related logs
      console.group('User Details');
      console.log('Name:', 'John');
      console.log('Age:', 30);
      console.log('Email:', 'john@example.com');
      console.groupEnd();
      
      // Collapsed group
      console.groupCollapsed('Advanced Settings');
      console.log('Setting 1:', true);
      console.log('Setting 2:', false);
      console.groupEnd();
      
      // Table display
      const users = [
        { name: 'John', age: 30, city: 'NYC' },
        { name: 'Jane', age: 25, city: 'LA' },
        { name: 'Bob', age: 35, city: 'Chicago' }
      ];
      
      console.table(users);
      console.table(users, ['name', 'age']); // Select columns
      
      // Count occurrences
      console.count('Button clicked');
      console.count('Button clicked');
      console.count('Button clicked');
      // Button clicked: 1
      // Button clicked: 2
      // Button clicked: 3
      
      console.countReset('Button clicked');
      
      // Timing
      console.time('Operation');
      // ... some operation
      console.timeLog('Operation', 'Checkpoint');
      // ... more work
      console.timeEnd('Operation');
      
      // Assertions
      console.assert(true, 'This is fine'); // Nothing logged
      console.assert(false, 'This will show'); // Logs error
      console.assert(1 === 2, 'Math is broken');
      
      // Trace - show call stack
      function foo() {
        function bar() {
          console.trace('Trace from bar');
        }
        bar();
      }
      foo();
      
      // Clear console
      console.clear();
      
      // Custom logger
      class Logger {
        constructor(prefix) {
          this.prefix = prefix;
        }
      
        log(message) {
          console.log(`[${this.prefix}]`, message);
        }
      
        error(message) {
          console.error(`[${this.prefix}]`, message);
        }
      
        warn(message) {
          console.warn(`[${this.prefix}]`, message);
        }
      }
      
      const logger = new Logger('MyApp');
      logger.log('Application started');
      logger.error('Something failed');

      Debugger Statement

      The debugger statement pauses execution for debugging in browser DevTools.

      Using Debugger
      // Debugger statement pauses execution
      function calculateTotal(items) {
        let total = 0;
      
        debugger; // Execution pauses here if DevTools open
      
        for (const item of items) {
          total += item.price * item.quantity;
        }
      
        return total;
      }
      
      // Conditional debugging
      function processData(data) {
        if (data.length > 1000) {
          debugger; // Only pause for large datasets
        }
      
        return data.map(item => item * 2);
      }
      
      // Debug complex conditions
      function findBug(arr) {
        for (let i = 0; i < arr.length; i++) {
          if (arr[i] < 0) {
            debugger; // Pause when negative found
          }
        }
      }
      
      // Production safeguard
      const DEBUG = process.env.NODE_ENV === 'development';
      
      function riskyOperation() {
        if (DEBUG) {
          debugger;
        }
      
        // Operation code
      }
      
      // Debug wrapper
      function debug(fn) {
        return function(...args) {
          debugger;
          return fn.apply(this, args);
        };
      }
      
      const debuggedFunction = debug(myFunction);
      
      // Breakpoint in catch
      try {
        riskyOperation();
      } catch (error) {
        debugger; // Inspect error state
        console.error(error);
      }

      Breakpoints and DevTools

      Browser DevTools provide powerful debugging features beyond code-based debugging.

      DevTools Debugging
      // DevTools Breakpoints:
      // 1. Line breakpoints - Click line number
      // 2. Conditional breakpoints - Right-click line number
      // 3. Logpoints - Log without pausing
      // 4. DOM breakpoints - Break on DOM changes
      // 5. Event listener breakpoints - Break on events
      // 6. XHR breakpoints - Break on network requests
      
      // Conditional breakpoint example
      function processItem(item) {
        // Set breakpoint with condition: item.id === 123
        console.log('Processing:', item.id);
        return item.value * 2;
      }
      
      // Watch expressions in DevTools
      // Add expressions to watch panel to monitor values
      
      // Call stack navigation
      function level3() {
        // When paused here, see full call stack
        return 'result';
      }
      
      function level2() {
        return level3();
      }
      
      function level1() {
        return level2();
      }
      
      // Scope inspection
      function outer() {
        const outerVar = 'outer';
      
        function inner() {
          const innerVar = 'inner';
          debugger; // Inspect both scopes
        }
      
        inner();
      }
      
      // Step through code
      // - Step over (F10): Execute current line
      // - Step into (F11): Enter function call
      // - Step out (Shift+F11): Exit current function
      // - Continue (F8): Resume execution
      
      // Console evaluation during pause
      // Type expressions in console to inspect state
      // e.g., variables, function calls, etc.
      
      // Source maps
      // Enable in DevTools to debug original source
      // Even when code is minified/transpiled
      
      // Network debugging
      // Monitor fetch requests in Network tab
      // Set XHR breakpoints for API calls
      
      // Performance profiling
      // Use Performance tab to find bottlenecks
      // Record and analyze frame rates
      
      // Memory leaks
      // Use Memory tab to detect leaks
      // Take heap snapshots and compare

      Source Maps

      Source maps enable debugging of minified/transpiled code by mapping back to original source.

      Source Map Usage
      // Webpack source map configuration
      // webpack.config.js
      module.exports = {
        mode: 'development',
        devtool: 'source-map', // Generate source maps
        // Other options:
        // 'eval' - fastest, lowest quality
        // 'inline-source-map' - embedded in bundle
        // 'hidden-source-map' - no reference in bundle
        // 'nosources-source-map' - no source code
      };
      
      // Vite source map
      // vite.config.js
      export default {
        build: {
          sourcemap: true
        }
      };
      
      // Source map comment in generated file
      // At end of minified file:
      //# sourceMappingURL=app.min.js.map
      
      // Source map file (app.min.js.map)
      {
        "version": 3,
        "sources": ["src/app.js", "src/utils.js"],
        "names": ["myFunction", "result"],
        "mappings": "AAAA,SAASA,WAAW..."
      }
      
      // Production considerations
      // Option 1: Don't deploy source maps
      // Option 2: Deploy to separate server
      // Option 3: Require authentication
      
      // Error reporting with source maps
      // Services like Sentry can use source maps
      // to show original code in error reports

      Debugging Best Practices

      Effective debugging strategies and patterns for faster problem resolution.

      Debugging Strategies
      // 1. Reproduce the bug consistently
      function reproduceBug() {
        // Document exact steps to trigger bug
        // Create minimal test case
        // Isolate the problem
      }
      
      // 2. Use meaningful log messages
      // ❌ Bad
      console.log(data);
      
      // ✅ Good
      console.log('API response received:', data);
      console.log('User validation failed:', {
        user,
        validationErrors
      });
      
      // 3. Log at strategic points
      function processOrder(order) {
        console.log('Processing order:', order.id);
      
        const validated = validateOrder(order);
        console.log('Validation result:', validated);
      
        const saved = saveOrder(order);
        console.log('Save result:', saved);
      
        return saved;
      }
      
      // 4. Use try/catch strategically
      async function robustFetch(url) {
        try {
          const response = await fetch(url);
          console.log('Response status:', response.status);
      
          if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
          }
      
          const data = await response.json();
          console.log('Data received:', data);
      
          return data;
        } catch (error) {
          console.error('Fetch failed:', {
            url,
            error: error.message,
            stack: error.stack
          });
          throw error;
        }
      }
      
      // 5. Defensive programming
      function safeOperation(value) {
        // Validate inputs
        if (!value) {
          console.warn('Invalid value provided');
          return null;
        }
      
        // Check preconditions
        if (typeof value !== 'number') {
          throw new TypeError('Expected number');
        }
      
        // Perform operation
        return value * 2;
      }
      
      // 6. Use assertions for assumptions
      function divide(a, b) {
        console.assert(typeof a === 'number', 'a must be number');
        console.assert(typeof b === 'number', 'b must be number');
        console.assert(b !== 0, 'b cannot be zero');
      
        return a / b;
      }
      
      // 7. Create helper debugging functions
      function debugObject(obj, label = 'Object') {
        console.group(label);
        console.log('Type:', typeof obj);
        console.log('Keys:', Object.keys(obj));
        console.log('Values:', Object.values(obj));
        console.table(obj);
        console.groupEnd();
      }
      
      // 8. Time operations
      function timeOperation(fn, label) {
        console.time(label);
        const result = fn();
        console.timeEnd(label);
        return result;
      }
      
      // 9. Binary search debugging
      // Comment out half of code to isolate issue
      // Repeat until you find the problematic section

      Practice Exercises

      1. Error Handler: Build a global error handler that catches and logs all unhandled errors
      2. Custom Logger: Create a logger class with different log levels (DEBUG, INFO, WARN, ERROR)
      3. Error Boundary: Implement an error boundary component that catches errors in child components
      4. Debug Helper: Build a debug toolbar that shows performance metrics and logs
      5. Stack Trace Parser: Parse stack traces and format them for better readability
      6. Error Reporter: Create a system that reports errors to a server with context information
      Key Takeaways:
      • Always use try/catch for error-prone operations (parsing, network, file I/O)
      • Throw specific error types (TypeError, RangeError) for better error handling
      • Create custom error classes for application-specific errors
      • Use console methods beyond log: error, warn, table, group, time
      • debugger statement is powerful for pausing execution during debugging
      • DevTools breakpoints are more flexible than debugger statements
      • Source maps enable debugging of minified/transpiled code
      • Log meaningful messages with context, not just values
      What's Next? Continue your learning journey:

      Modules & Tooling

      Modern JavaScript project structure, modules, and build tools

      Introduction: Modern JavaScript development relies on modules for code organization and build tools for optimization. ES Modules provide a standard way to structure code into reusable pieces, while tools like npm, Webpack, and Vite handle dependencies, bundling, and deployment. Understanding this ecosystem is essential for building scalable, maintainable applications and working with modern frameworks.

      ES Modules: Import and Export

      ES Modules (ESM) provide a standardized module system built into JavaScript for organizing code.

      Module Basics
      // math.js - Named exports
      export function add(a, b) {
        return a + b;
      }
      
      export function subtract(a, b) {
        return a - b;
      }
      
      export const PI = 3.14159;
      
      // app.js - Import named exports
      import { add, subtract, PI } from './math.js';
      console.log(add(5, 3)); // 8
      
      // Import everything as namespace
      import * as Math from './math.js';
      console.log(Math.add(5, 3));
      console.log(Math.PI);
      
      // Export list syntax
      // utils.js
      function multiply(a, b) {
        return a * b;
      }
      
      function divide(a, b) {
        return a / b;
      }
      
      export { multiply, divide };
      
      // Import with rename
      import { multiply as mult } from './utils.js';
      
      // Re-export from another module
      export { add, subtract } from './math.js';
      export * from './math.js';

      Default vs Named Exports

      Default exports provide a single main export per module, while named exports allow multiple exports.

      Export Patterns
      // user.js - Default export
      export default class User {
        constructor(name) {
          this.name = name;
        }
      
        greet() {
          return `Hello, ${this.name}!`;
        }
      }
      
      // Import default (any name works)
      import User from './user.js';
      import MyUser from './user.js'; // Same thing
      
      const user = new User('John');
      
      // Mixed: default + named exports
      // config.js
      export default {
        apiUrl: 'https://api.example.com',
        timeout: 5000
      };
      
      export const VERSION = '1.0.0';
      export const DEBUG = true;
      
      // Import both
      import config, { VERSION, DEBUG } from './config.js';
      
      // Default export patterns
      // Function
      export default function greet(name) {
        return `Hello, ${name}!`;
      }
      
      // Object
      export default {
        add: (a, b) => a + b,
        subtract: (a, b) => a - b
      };
      
      // Class
      export default class Calculator {
        add(a, b) { return a + b; }
      }
      
      // Best practices
      // ❌ Don't mix default with many named exports
      // ❌ export default { func1, func2, func3, func4, func5 };
      
      // ✅ Use named exports for multiple utilities
      export { func1, func2, func3, func4, func5 };
      
      // ✅ Use default for main component/class
      export default class MainComponent {}

      Export Renaming

      Rename exports and imports to avoid naming conflicts and improve clarity.

      Renaming Exports and Imports
      // Export with rename
      // validators.js
      function validateEmail(email) {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
      }
      
      function validatePhone(phone) {
        return /^\d{10}$/.test(phone);
      }
      
      export {
        validateEmail as isValidEmail,
        validatePhone as isValidPhone
      };
      
      // Import with rename
      import { isValidEmail as checkEmail } from './validators.js';
      
      console.log(checkEmail('test@example.com'));
      
      // Avoid naming conflicts
      // services.js
      export function getUser() { /* API call */ }
      export function getPost() { /* API call */ }
      
      // helpers.js
      export function getUser() { /* Helper */ }
      
      // app.js - Rename to avoid conflict
      import { getUser as getUserFromAPI } from './services.js';
      import { getUser as getUserHelper } from './helpers.js';
      
      // Namespace pattern
      import * as Services from './services.js';
      import * as Helpers from './helpers.js';
      
      Services.getUser();
      Helpers.getUser();

      Dynamic Imports

      Load modules dynamically at runtime for code splitting and lazy loading.

      Dynamic Import Syntax
      // Static import (always loaded)
      import { heavyFunction } from './heavy.js';
      
      // Dynamic import (load on demand)
      button.addEventListener('click', async () => {
        const module = await import('./heavy.js');
        module.heavyFunction();
      });
      
      // With destructuring
      button.addEventListener('click', async () => {
        const { heavyFunction } = await import('./heavy.js');
        heavyFunction();
      });
      
      // Conditional loading
      async function loadTheme(isDark) {
        if (isDark) {
          const { darkTheme } = await import('./themes/dark.js');
          applyTheme(darkTheme);
        } else {
          const { lightTheme } = await import('./themes/light.js');
          applyTheme(lightTheme);
        }
      }
      
      // Error handling
      try {
        const module = await import('./module.js');
        module.init();
      } catch (error) {
        console.error('Failed to load module:', error);
      }
      
      // Lazy load route components
      const routes = {
        '/home': () => import('./pages/Home.js'),
        '/about': () => import('./pages/About.js'),
        '/contact': () => import('./pages/Contact.js')
      };
      
      async function navigate(path) {
        const loadComponent = routes[path];
        if (loadComponent) {
          const { default: Component } = await loadComponent();
          renderComponent(Component);
        }
      }
      
      // Pre-loading
      const preloadModule = import('./module.js');
      // Later...
      const module = await preloadModule;

      Tree Shaking

      Modern bundlers eliminate unused code (dead code elimination) for smaller bundles.

      Tree Shaking Optimization
      // utils.js - Export individual functions
      export function used() {
        return 'This is used';
      }
      
      export function unused() {
        return 'This will be removed';
      }
      
      export function alsoUnused() {
        return 'This too';
      }
      
      // app.js - Only import what you need
      import { used } from './utils.js';
      
      console.log(used());
      // unused() and alsoUnused() will be removed by bundler
      
      // ❌ Bad for tree shaking - imports everything
      import * as Utils from './utils.js';
      
      // ✅ Good for tree shaking - imports only used
      import { used } from './utils.js';
      
      // ❌ Default exports harder to tree shake
      export default {
        used: () => {},
        unused: () => {},
        alsoUnused: () => {}
      };
      
      // ✅ Named exports enable better tree shaking
      export function used() {}
      export function unused() {}
      
      // Side effects
      // If module has side effects, mark it in package.json
      // package.json
      {
        "sideEffects": false // No side effects, safe to tree shake
      }
      
      // Or specify files with side effects
      {
        "sideEffects": ["*.css", "./src/polyfills.js"]
      }

      npm and Package Management

      npm is the package manager for JavaScript, managing dependencies and scripts.

      npm Basics
      // Initialize new project
      // npm init
      // npm init -y (skip questions)
      
      // package.json structure
      {
        "name": "my-project",
        "version": "1.0.0",
        "description": "My JavaScript project",
        "main": "index.js",
        "scripts": {
          "start": "node index.js",
          "build": "webpack",
          "test": "jest",
          "dev": "webpack serve --mode development"
        },
        "dependencies": {
          "lodash": "^4.17.21",
          "axios": "^1.6.0"
        },
        "devDependencies": {
          "webpack": "^5.89.0",
          "jest": "^29.7.0"
        }
      }
      
      // Install packages
      // npm install lodash
      // npm install --save-dev webpack (dev dependency)
      // npm install -g npm (global)
      
      // Semver versions
      // ^1.2.3 - Compatible (1.x.x)
      // ~1.2.3 - Approximately (1.2.x)
      // 1.2.3 - Exact version
      // * or latest - Latest version
      
      // Using installed packages
      import _ from 'lodash';
      import axios from 'axios';
      
      const nums = [1, 2, 3, 4, 5];
      console.log(_.sum(nums));
      
      // Scripts
      // npm start
      // npm run build
      // npm test
      // npm run dev
      
      // Useful commands
      // npm list - Show installed packages
      // npm outdated - Check for updates
      // npm update - Update packages
      // npm uninstall lodash - Remove package
      // npm ci - Clean install (CI/CD)

      Scripts and Build Tasks

      Define custom scripts in package.json for common development tasks.

      Package Scripts
      // package.json
      {
        "scripts": {
          "start": "node server.js",
          "dev": "nodemon server.js",
          "build": "webpack --mode production",
          "build:dev": "webpack --mode development",
          "test": "jest",
          "test:watch": "jest --watch",
          "test:coverage": "jest --coverage",
          "lint": "eslint src/**/*.js",
          "lint:fix": "eslint src/**/*.js --fix",
          "format": "prettier --write 'src/**/*.js'",
          "clean": "rm -rf dist",
          "prebuild": "npm run clean",
          "postbuild": "npm run test",
          "deploy": "npm run build && npm run upload"
        }
      }
      
      // Run scripts
      // npm start (reserved name, no 'run' needed)
      // npm test (reserved name)
      // npm run build
      // npm run lint:fix
      
      // Pre and post hooks
      // prebuild runs before build
      // postbuild runs after build
      
      // Pass arguments
      // npm run build -- --watch
      // package.json: "build": "webpack"
      // Executes: webpack --watch
      
      // Environment variables
      {
        "scripts": {
          "start:prod": "NODE_ENV=production node server.js",
          "start:dev": "NODE_ENV=development node server.js"
        }
      }
      
      // Cross-platform with cross-env
      {
        "scripts": {
          "start": "cross-env NODE_ENV=production node server.js"
        }
      }

      Dependencies and DevDependencies

      Understand the difference between production and development dependencies.

      Dependency Types
      // dependencies - Required in production
      {
        "dependencies": {
          "express": "^4.18.0",
          "axios": "^1.6.0",
          "lodash": "^4.17.21"
        }
      }
      
      // Install: npm install express
      
      // devDependencies - Only for development
      {
        "devDependencies": {
          "webpack": "^5.89.0",
          "babel-loader": "^9.1.0",
          "jest": "^29.7.0",
          "eslint": "^8.55.0",
          "prettier": "^3.1.0"
        }
      }
      
      // Install: npm install --save-dev webpack
      
      // peerDependencies - Required by consumers
      {
        "peerDependencies": {
          "react": "^18.0.0"
        }
      }
      
      // optionalDependencies - Nice to have
      {
        "optionalDependencies": {
          "fsevents": "^2.3.0"
        }
      }
      
      // When to use what?
      /*
      dependencies:
      - Runtime libraries (lodash, axios)
      - Framework code (react, vue)
      - Server dependencies (express)
      
      devDependencies:
      - Build tools (webpack, vite)
      - Testing (jest, mocha)
      - Linting (eslint, prettier)
      - Dev servers
      */
      
      // Production install (skip devDependencies)
      // npm install --production
      // npm ci --production

      Bundlers: Webpack, Vite, Parcel

      Bundlers combine modules, optimize code, and prepare for production deployment.

      Bundler Overview
      // Webpack - Most configurable
      // webpack.config.js
      module.exports = {
        entry: './src/index.js',
        output: {
          filename: 'bundle.js',
          path: __dirname + '/dist'
        },
        module: {
          rules: [
            {
              test: /\.js$/,
              exclude: /node_modules/,
              use: 'babel-loader'
            },
            {
              test: /\.css$/,
              use: ['style-loader', 'css-loader']
            }
          ]
        },
        plugins: [
          new HtmlWebpackPlugin({
            template: './src/index.html'
          })
        ]
      };
      
      // Vite - Fast and modern
      // vite.config.js
      import { defineConfig } from 'vite';
      
      export default defineConfig({
        root: './src',
        build: {
          outDir: '../dist'
        },
        server: {
          port: 3000
        }
      });
      
      // Parcel - Zero config
      // No config needed! Just:
      // parcel index.html
      
      // Comparison
      const comparison = {
        webpack: {
          speed: 'Slower',
          config: 'Complex',
          features: 'Most comprehensive',
          useCase: 'Large, complex projects'
        },
        vite: {
          speed: 'Very fast (ESM + esbuild)',
          config: 'Simple',
          features: 'Modern, opinionated',
          useCase: 'Modern frameworks, fast dev'
        },
        parcel: {
          speed: 'Fast',
          config: 'Zero config',
          features: 'Automatic',
          useCase: 'Quick projects, prototypes'
        }
      };
      
      // Common features
      /*
      - Code splitting
      - Tree shaking
      - Minification
      - Source maps
      - Hot Module Replacement (HMR)
      - Asset optimization
      - CSS/Image processing
      */

      Practice Exercises

      1. Module Library: Create a utility library with multiple modules and export/import patterns
      2. Dynamic Router: Build a router that lazy-loads route components using dynamic imports
      3. npm Package: Create and publish a simple npm package with proper package.json configuration
      4. Build Pipeline: Set up a build process with webpack or vite including CSS and asset handling
      5. Monorepo Structure: Organize a project with multiple packages using workspaces
      6. Bundle Analyzer: Use bundle analysis tools to identify and optimize large dependencies
      Key Takeaways:
      • ES Modules use import/export for modern, standardized code organization
      • Named exports enable tree shaking; use for multiple utilities
      • Default exports best for single main export (component, class)
      • Dynamic imports enable code splitting and lazy loading
      • npm manages dependencies via package.json
      • Use devDependencies for build tools, dependencies for runtime code
      • Bundlers (Webpack/Vite/Parcel) optimize code for production
      • Tree shaking eliminates unused code for smaller bundles
      What's Next? Continue your learning journey:

      Classes & OOP

      Use modern class syntax to model data, behavior, and relationships.

      ES6 classes are syntax sugar over prototypes. Define constructors, instance methods, getters/setters, statics, and inheritance hierarchies. Use them when they improve clarity, otherwise prefer small composable functions.

      Class Declaration Basics

      Declare classes with constructors and methods. Instances are created via new.

      Simple Class
      class User {
        constructor(name, role = 'viewer') {
          this.name = name;
          this.role = role;
        }
      
        describe() {
          return `${this.name} (${this.role})`;
        }
      }
      
      const u = new User('Rae', 'admin');
      console.log(u.describe());

      Getters and Setters

      Encapsulate derived values or validation logic using getter and setter syntax.

      Computed Properties
      class Rectangle {
        constructor(width, height) {
          this.width = width;
          this.height = height;
        }
      
        get area() {
          return this.width * this.height;
        }
      
        set area(value) {
          const side = Math.sqrt(value);
          this.width = side;
          this.height = side;
        }
      }
      
      const square = new Rectangle(4, 4);
      console.log(square.area);
      square.area = 81;
      console.log(square.width, square.height);

      Static Methods and Properties

      Statics live on the class constructor itself, not instances. Use them for utilities or factories.

      Static Helpers
      class Id {
        static prefix = 'usr';
        static nextId = 1;
      
        static generate() {
          return `${this.prefix}-${this.nextId++}`;
        }
      }
      
      console.log(Id.generate());
      console.log(Id.generate());

      Inheritance and super

      Extend classes to reuse behavior. Call super to invoke parent constructors or methods.

      Extending
      class Service {
        constructor(name) {
          this.name = name;
        }
        status() {
          return `${this.name} ready`;
        }
      }
      
      class EmailService extends Service {
        constructor(name, sender) {
          super(name);
          this.sender = sender;
        }
      
        status() {
          return `${super.status()} from ${this.sender}`;
        }
      }
      
      const mailer = new EmailService('Mailer', 'noreply@example.com');
      console.log(mailer.status());

      Private Fields and Methods

      Use # prefixed fields for per-instance privacy.

      Encapsulated State
      class Counter {
        #value = 0;
        increment() {
          this.#value++;
          return this.#value;
        }
        get value() {
          return this.#value;
        }
      }
      
      const c = new Counter();
      console.log(c.increment());
      console.log(c.value);
      // c.#value; // SyntaxError

      Composition vs Inheritance

      Favor composition to mix capabilities without deep hierarchies.

      Composable Traits
      const canLog = state => ({
        log(msg) {
          state.logs.push(msg);
          return state.logs;
        }
      });
      
      const canToggle = state => ({
        toggle() {
          state.enabled = !state.enabled;
          return state.enabled;
        }
      });
      
      const createFeature = name => {
        const state = { name, enabled: false, logs: [] };
        return { ...state, ...canLog(state), ...canToggle(state) };
      };
      
      console.log(createFeature('Search').toggle());

      Common Pitfalls

      Remember that class methods are not auto-bound; losing this context can break code.

      Binding Methods
      class Button {
        constructor(label) {
          this.label = label;
          this.handleClick = this.handleClick.bind(this);
        }
        handleClick() {
          return `${this.label} clicked`;
        }
      }
      
      const b = new Button('Save');
      const handler = b.handleClick;
      console.log(handler());
      + +

      Class Fields and Arrow Methods

      +

      Public fields and arrow methods are initialized per instance and auto-bind this.

      +
      +
      Field Declarations
      +
      class Toggle {
      +  state = false;
      +  label;
      +
      +  constructor(label) {
      +    this.label = label;
      +  }
      +
      +  flip = () => {
      +    this.state = !this.state;
      +    return `${this.label}: ${this.state}`;
      +  };
      +}
      +
      +const t = new Toggle('Feature');
      +const action = t.flip;
      +console.log(action()); // still bound
      +
      + +

      Design Considerations

      +

      Keep classes small, single-purpose, and favor dependency injection for testability.

      +
      +
      Inject Dependencies
      +
      class ReportService {
      +  constructor(fetcher) {
      +    this.fetcher = fetcher;
      +  }
      +
      +  async load(id) {
      +    const data = await this.fetcher(`/reports/${id}`);
      +    return data;
      +  }
      +}
      +
      +const service = new ReportService((url) => Promise.resolve({ url }));
      +service.load(7).then(console.log);
      +

      Practice Exercises

      1. Create a class with a constructor and an instance method; instantiate it twice and compare method references.
      2. Implement getters and setters for a computed property like full name.
      3. Add static methods to generate IDs or cache instances.
      4. Build a base class and extend it with super calls; override a method while reusing parent logic.
      5. Use private fields to protect internal counters; expose read-only getters.
      6. Demonstrate method binding to preserve this when passing callbacks to setTimeout.
      7. Refactor an inheritance hierarchy into composition using factory functions.
      8. Create a mixin utility that copies methods onto a class prototype.
      9. Override toString() on a class to customize logging.
      10. Write unit tests for a class constructor and its methods to verify behavior.
      Key Takeaways:
      • Classes wrap prototype mechanics with clearer syntax.
      • Constructors set instance state; methods live on the prototype unless defined as fields.
      • Getters/setters encapsulate derived values, while static members live on the constructor.
      • Inheritance uses extends and super; composition remains a flexible alternative.
      • Bind methods when passing them as callbacks to preserve this.

      What's Next?

      Continue with Promises and Async/Await to orchestrate asynchronous flows, or revisit Prototypes and Inheritance for the underlying mechanics.

      Prototypes & Inheritance

      Share behavior across objects with prototype chains and reusable blueprints.

      JavaScript uses prototypes instead of classical classes at its core. Every object links to a prototype that supplies shared properties. Understand __proto__, constructor functions, Object.create, and how to attach methods to the prototype to avoid duplication.

      Prototype Chain Basics

      Objects delegate property lookups to their prototype via the internal [[Prototype]] link.

      Delegation
      const base = { alive: true };
      const user = { name: 'Ada', __proto__: base };
      
      console.log(user.name);
      console.log(user.alive); // found on base
      Object.getPrototypeOf
      const proto = Object.getPrototypeOf(user);
      console.log(proto === base);

      Object.create for Clean Prototypes

      Create objects with a chosen prototype without invoking constructors.

      Factory with Object.create
      const personProto = {
        describe() {
          return `${this.name} (${this.role})`;
        }
      };
      
      function makePerson(name, role) {
        const person = Object.create(personProto);
        person.name = name;
        person.role = role;
        return person;
      }
      
      const dev = makePerson('Lin', 'Engineer');
      console.log(dev.describe());

      Constructor Functions

      Before classes, constructor functions paired with new set up instances and prototypes.

      Defining a Constructor
      function Car(make, model) {
        this.make = make;
        this.model = model;
      }
      
      Car.prototype.honk = function() {
        return `${this.make} ${this.model} says beep`;
      };
      
      const car = new Car('Honda', 'Civic');
      console.log(car.honk());
      Instance vs Prototype Props
      car.wheels = 4;
      
      console.log(car.wheels); // own property
      console.log(car.honk === Car.prototype.honk); // shared

      Inheritance Between Constructors

      Link prototypes to share behavior across hierarchies.

      Subclassing
      function Vehicle(type) {
        this.type = type;
      }
      
      Vehicle.prototype.describe = function() {
        return `Type: ${this.type}`;
      };
      
      function Truck(make, capacity) {
        Vehicle.call(this, 'truck');
        this.make = make;
        this.capacity = capacity;
      }
      
      Truck.prototype = Object.create(Vehicle.prototype);
      Truck.prototype.constructor = Truck;
      
      Truck.prototype.load = function(amount) {
        return `${this.make} loading ${amount} tons`;
      };
      
      const t = new Truck('Volvo', 10);
      console.log(t.describe());
      console.log(t.load(5));

      Changing Prototypes Safely

      Prefer Object.setPrototypeOf for updates; avoid mutating __proto__ in performance-sensitive code.

      setPrototypeOf
      const metrics = { track() { return 'tracking'; } };
      const feature = { name: 'Search' };
      
      Object.setPrototypeOf(feature, metrics);
      console.log(feature.track());

      Detecting Properties

      Differentiate own properties from inherited ones when iterating.

      hasOwnProperty vs in
      console.log('honk' in car); // true (inherited)
      console.log(car.hasOwnProperty('honk')); // false
      
      console.log(car.hasOwnProperty('wheels')); // true

      Pitfalls and Best Practices

      Avoid overwriting prototypes after instances exist; attach methods once to save memory.

      Method Placement
      function Widget(name) {
        this.name = name;
        // this.render = () => `${this.name}`; // new function per instance
      }
      
      Widget.prototype.render = function() {
        return `Render ${this.name}`;
      };
      
      const w1 = new Widget('Chart');
      const w2 = new Widget('Table');
      console.log(w1.render === w2.render); // true

      Prototypes and Modern Classes

      ES6 classes wrap prototype mechanics with clearer syntax but retain the same inheritance model.

      Class Sugar
      class Animal {
        speak() {
          return 'noise';
        }
      }
      
      class Dog extends Animal {
        speak() {
          return 'woof';
        }
      }
      
      const d = new Dog();
      console.log(d.speak());
      console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);
      + +

      Polymorphism and Overrides

      +

      Derived prototypes override shared methods to specialize behavior while keeping a common interface.

      +
      +
      Shared Interface
      +
      function Notifier() {}
      +Notifier.prototype.send = function(message) {
      +  return `Sending: ${message}`;
      +};
      +
      +function EmailNotifier() {}
      +EmailNotifier.prototype = Object.create(Notifier.prototype);
      +EmailNotifier.prototype.constructor = EmailNotifier;
      +EmailNotifier.prototype.send = function(message) {
      +  return `Email -> ${message}`;
      +};
      +
      +function SmsNotifier() {}
      +SmsNotifier.prototype = Object.create(Notifier.prototype);
      +SmsNotifier.prototype.constructor = SmsNotifier;
      +SmsNotifier.prototype.send = function(message) {
      +  return `SMS -> ${message}`;
      +};
      +
      +const channels = [new EmailNotifier(), new SmsNotifier()];
      +channels.forEach(channel => console.log(channel.send('Alert!')));
      +
      + +

      Avoiding Prototype Pollution

      +

      Never modify Object.prototype in shared code; it can break iteration and third-party libraries.

      +
      +
      Do Not Extend Object.prototype
      +
      // Avoid doing this globally
      +// Object.prototype.log = function() { console.log(this); };
      +
      +const safeHelpers = {
      +  log(obj) {
      +    console.log(obj);
      +  }
      +};
      +
      +safeHelpers.log({ ok: true });
      +

      Practice Exercises

      1. Create an object with Object.create and add shared methods to its prototype object.
      2. Write a constructor function and attach methods to its prototype; verify instances share them.
      3. Implement inheritance between two constructors using Object.create and reset constructor.
      4. Demonstrate the difference between own and inherited properties using in and hasOwnProperty.
      5. Refactor duplicated methods defined in a constructor into prototype methods.
      6. Use Object.getPrototypeOf to inspect an object's chain and log each step until null.
      7. Change an object's prototype at runtime with Object.setPrototypeOf and observe new behavior.
      8. Compare memory usage by creating many instances with prototype methods versus inline methods (use console timing).
      9. Extend a base prototype with a new method and verify existing instances gain access.
      10. Translate a constructor-based hierarchy into an ES6 class hierarchy and confirm results match.
      Key Takeaways:
      • Objects delegate property lookups through their prototype chain.
      • Object.create builds objects with explicit prototypes; constructor functions plus new set instance state.
      • Attach shared methods to prototypes to save memory and enable inheritance.
      • Use Object.setPrototypeOf cautiously and prefer setup-time links.
      • ES6 classes are syntax sugar over the same prototype system.

      What's Next?

      Proceed to Classes and OOP to see modern syntax for prototypes, or revisit Scope and Hoisting to understand how prototype methods access variables.

      Regular Expressions and Strings

      Regex helps search and replace text. Use flags like i and g for case-insensitive and global matches.

      Regex Replace
      const text = 'Email me at test@example.com';
      const masked = text.replace(/\b[\w.-]+@[\w.-]+/gi, '[hidden]');
      

      Dates & Times

      Master date manipulation, formatting, and timezone handling

      Introduction: Working with dates and times is a fundamental skill in JavaScript development. From displaying timestamps to calculating durations and handling timezones, the Date object and modern APIs provide powerful tools. While JavaScript's built-in Date has limitations, understanding its core functionality—combined with modern libraries like date-fns or Day.js—enables you to handle any temporal requirement in your applications.

      Date Constructor and Creation

      Create Date objects using various methods for different use cases.

      Creating Dates
      // Current date and time
      const now = new Date();
      console.log(now); // Current date/time
      
      // From timestamp (milliseconds since Jan 1, 1970 UTC)
      const fromTimestamp = new Date(1705312800000);
      console.log(fromTimestamp);
      
      // From date string
      const fromString = new Date('2024-01-15');
      const fromISO = new Date('2024-01-15T10:30:00Z');
      console.log(fromString);
      
      // From date components (month is 0-indexed!)
      const fromComponents = new Date(2024, 0, 15); // Jan 15, 2024
      const withTime = new Date(2024, 0, 15, 10, 30, 0, 0); // 10:30:00.000
      
      // Month is 0-indexed: 0=Jan, 11=Dec
      console.log(new Date(2024, 0, 1)); // January 1
      console.log(new Date(2024, 11, 31)); // December 31
      
      // Invalid dates
      const invalid = new Date('invalid');
      console.log(invalid); // Invalid Date
      console.log(isNaN(invalid)); // true
      
      // Check if date is valid
      function isValidDate(date) {
        return date instanceof Date && !isNaN(date);
      }
      
      console.log(isValidDate(new Date())); // true
      console.log(isValidDate(new Date('invalid'))); // false
      
      // Copy a date
      const original = new Date();
      const copy = new Date(original);
      console.log(copy.getTime() === original.getTime()); // true
      
      // UTC dates
      const utcDate = new Date(Date.UTC(2024, 0, 15, 10, 30));
      console.log(utcDate.toISOString());

      Timestamps and Epoch Time

      Work with timestamps for efficient date comparisons and storage.

      Timestamp Operations
      // Get current timestamp (ms since Jan 1, 1970)
      const timestamp1 = Date.now(); // Fastest
      const timestamp2 = new Date().getTime();
      const timestamp3 = +new Date(); // Unary plus operator
      
      console.log(timestamp1); // e.g., 1705312800000
      
      // Convert date to timestamp
      const date = new Date('2024-01-15');
      const ms = date.getTime();
      console.log(ms);
      
      // Convert timestamp to date
      const dateFromMs = new Date(1705312800000);
      console.log(dateFromMs);
      
      // Unix timestamp (seconds since epoch)
      const unixTimestamp = Math.floor(Date.now() / 1000);
      console.log(unixTimestamp); // e.g., 1705312800
      
      // Compare dates using timestamps
      const date1 = new Date('2024-01-15');
      const date2 = new Date('2024-01-20');
      
      if (date1.getTime() < date2.getTime()) {
        console.log('date1 is before date2');
      }
      
      // Calculate difference in milliseconds
      const diff = date2 - date1; // 432000000 ms
      console.log(diff);
      
      // Convert to days
      const days = diff / (1000 * 60 * 60 * 24);
      console.log(`${days} days`); // 5 days
      
      // Measure execution time
      const startTime = Date.now();
      
      // Some operation
      for (let i = 0; i < 1000000; i++) {}
      
      const endTime = Date.now();
      console.log(`Execution took ${endTime - startTime}ms`);
      
      // Performance.now() for high-precision timing
      const start = performance.now();
      // Operation
      const end = performance.now();
      console.log(`Precise timing: ${end - start}ms`);

      Getters and Setters

      Extract and modify individual date components with getter and setter methods.

      Date Component Methods
      const date = new Date('2024-01-15T10:30:45.123');
      
      // Getters (local time)
      console.log(date.getFullYear()); // 2024
      console.log(date.getMonth()); // 0 (January, 0-indexed!)
      console.log(date.getDate()); // 15 (day of month)
      console.log(date.getDay()); // 1 (day of week: 0=Sun, 6=Sat)
      console.log(date.getHours()); // 10
      console.log(date.getMinutes()); // 30
      console.log(date.getSeconds()); // 45
      console.log(date.getMilliseconds()); // 123
      
      // UTC getters
      console.log(date.getUTCFullYear()); // 2024
      console.log(date.getUTCMonth()); // 0
      console.log(date.getUTCDate()); // 15
      console.log(date.getUTCHours()); // May differ from local
      
      // Setters (local time)
      const modifiable = new Date('2024-01-15');
      modifiable.setFullYear(2025);
      modifiable.setMonth(11); // December
      modifiable.setDate(25);
      modifiable.setHours(14, 30, 0, 0); // 2:30:00 PM
      
      console.log(modifiable); // 2025-12-25 14:30:00
      
      // UTC setters
      modifiable.setUTCHours(10);
      modifiable.setUTCMinutes(0);
      
      // Chaining setters
      const newDate = new Date()
        .setFullYear(2024);
      
      // Add/subtract days
      function addDays(date, days) {
        const result = new Date(date);
        result.setDate(result.getDate() + days);
        return result;
      }
      
      console.log(addDays(new Date(), 7)); // 7 days from now
      
      // Add/subtract months
      function addMonths(date, months) {
        const result = new Date(date);
        result.setMonth(result.getMonth() + months);
        return result;
      }
      
      console.log(addMonths(new Date(), 3)); // 3 months from now
      
      // Start of day
      function startOfDay(date) {
        const result = new Date(date);
        result.setHours(0, 0, 0, 0);
        return result;
      }
      
      // End of day
      function endOfDay(date) {
        const result = new Date(date);
        result.setHours(23, 59, 59, 999);
        return result;
      }
      
      // Start of month
      function startOfMonth(date) {
        return new Date(date.getFullYear(), date.getMonth(), 1);
      }
      
      // End of month
      function endOfMonth(date) {
        return new Date(date.getFullYear(), date.getMonth() + 1, 0);
      }

      Date Formatting

      Convert dates to human-readable strings in various formats.

      Date to String Conversion
      const date = new Date('2024-01-15T10:30:00');
      
      // ISO 8601 format (standard)
      console.log(date.toISOString()); // '2024-01-15T10:30:00.000Z'
      
      // Locale-specific formats
      console.log(date.toLocaleString()); // '1/15/2024, 10:30:00 AM' (US)
      console.log(date.toLocaleDateString()); // '1/15/2024'
      console.log(date.toLocaleTimeString()); // '10:30:00 AM'
      
      // Custom locale
      console.log(date.toLocaleDateString('en-GB')); // '15/01/2024'
      console.log(date.toLocaleDateString('de-DE')); // '15.1.2024'
      console.log(date.toLocaleDateString('ja-JP')); // '2024/1/15'
      
      // Format options
      const options = {
        weekday: 'long',
        year: 'numeric',
        month: 'long',
        day: 'numeric'
      };
      
      console.log(date.toLocaleDateString('en-US', options));
      // 'Monday, January 15, 2024'
      
      // Time format options
      const timeOptions = {
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
        hour12: false
      };
      
      console.log(date.toLocaleTimeString('en-US', timeOptions));
      // '10:30:00'
      
      // Combined date and time
      const fullOptions = {
        year: 'numeric',
        month: 'short',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit'
      };
      
      console.log(date.toLocaleString('en-US', fullOptions));
      // 'Jan 15, 2024, 10:30 AM'
      
      // Legacy methods (avoid)
      console.log(date.toString()); // Implementation-dependent
      console.log(date.toDateString()); // 'Mon Jan 15 2024'
      console.log(date.toTimeString()); // '10:30:00 GMT+0000'
      console.log(date.toUTCString()); // 'Mon, 15 Jan 2024 10:30:00 GMT'
      
      // Custom formatting function
      function formatDate(date, format) {
        const pad = (n) => String(n).padStart(2, '0');
      
        const replacements = {
          'YYYY': date.getFullYear(),
          'MM': pad(date.getMonth() + 1),
          'DD': pad(date.getDate()),
          'HH': pad(date.getHours()),
          'mm': pad(date.getMinutes()),
          'ss': pad(date.getSeconds())
        };
      
        return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => replacements[match]);
      }
      
      console.log(formatDate(date, 'YYYY-MM-DD HH:mm:ss'));
      // '2024-01-15 10:30:00'
      
      console.log(formatDate(date, 'DD/MM/YYYY'));
      // '15/01/2024'

      Date Parsing

      Parse date strings into Date objects, handling various formats.

      Parsing Date Strings
      // ISO 8601 (recommended - unambiguous)
      const iso = new Date('2024-01-15T10:30:00Z');
      console.log(iso);
      
      // Date.parse() returns timestamp
      const timestamp = Date.parse('2024-01-15');
      const date = new Date(timestamp);
      
      // Common formats (implementation-dependent!)
      const formats = [
        '2024-01-15',
        '01/15/2024',
        'January 15, 2024',
        '15 Jan 2024',
        '2024-01-15T10:30:00'
      ];
      
      formats.forEach(format => {
        const parsed = new Date(format);
        console.log(`${format} => ${parsed}`);
      });
      
      // Safe parsing with validation
      function parseDate(str) {
        const date = new Date(str);
        return isNaN(date) ? null : date;
      }
      
      const valid = parseDate('2024-01-15');
      const invalid = parseDate('invalid');
      
      console.log(valid); // Date object
      console.log(invalid); // null
      
      // Parse custom format
      function parseCustomDate(str, format = 'YYYY-MM-DD') {
        const parts = str.split(/[-/]/);
      
        if (format === 'YYYY-MM-DD') {
          return new Date(parts[0], parts[1] - 1, parts[2]);
        } else if (format === 'DD/MM/YYYY') {
          return new Date(parts[2], parts[1] - 1, parts[0]);
        }
      
        return null;
      }
      
      console.log(parseCustomDate('15/01/2024', 'DD/MM/YYYY'));
      
      // Parse relative dates
      function parseRelativeDate(str) {
        const now = new Date();
        const matches = str.match(/(\d+)\s+(day|week|month|year)s?\s+ago/);
      
        if (!matches) return null;
      
        const [, amount, unit] = matches;
        const num = parseInt(amount);
      
        const result = new Date(now);
      
        switch (unit) {
          case 'day':
            result.setDate(result.getDate() - num);
            break;
          case 'week':
            result.setDate(result.getDate() - num * 7);
            break;
          case 'month':
            result.setMonth(result.getMonth() - num);
            break;
          case 'year':
            result.setFullYear(result.getFullYear() - num);
            break;
        }
      
        return result;
      }
      
      console.log(parseRelativeDate('3 days ago'));
      console.log(parseRelativeDate('2 weeks ago'));
      console.log(parseRelativeDate('1 month ago'));

      Date Comparisons

      Compare dates for sorting, validation, and time-based logic.

      Comparing Dates
      const date1 = new Date('2024-01-15');
      const date2 = new Date('2024-01-20');
      const date3 = new Date('2024-01-15');
      
      // Direct comparison (compares timestamps)
      console.log(date1 < date2); // true
      console.log(date1 > date2); // false
      console.log(date1 <= date3); // true
      
      // Equality requires timestamp comparison
      console.log(date1 == date3); // false (different objects)
      console.log(date1.getTime() === date3.getTime()); // true
      
      // Helper functions
      function isSameDay(date1, date2) {
        return date1.getFullYear() === date2.getFullYear() &&
               date1.getMonth() === date2.getMonth() &&
               date1.getDate() === date2.getDate();
      }
      
      function isBefore(date1, date2) {
        return date1.getTime() < date2.getTime();
      }
      
      function isAfter(date1, date2) {
        return date1.getTime() > date2.getTime();
      }
      
      function isBetween(date, start, end) {
        return date >= start && date <= end;
      }
      
      // Check if date is today
      function isToday(date) {
        return isSameDay(date, new Date());
      }
      
      // Check if date is in the past
      function isPast(date) {
        return date < new Date();
      }
      
      // Check if date is in the future
      function isFuture(date) {
        return date > new Date();
      }
      
      // Sort dates
      const dates = [
        new Date('2024-03-15'),
        new Date('2024-01-15'),
        new Date('2024-02-15')
      ];
      
      dates.sort((a, b) => a - b); // Ascending
      console.log(dates);
      
      dates.sort((a, b) => b - a); // Descending
      console.log(dates);
      
      // Find min/max date
      const minDate = new Date(Math.min(...dates));
      const maxDate = new Date(Math.max(...dates));
      
      console.log('Earliest:', minDate);
      console.log('Latest:', maxDate);
      
      // Age calculation
      function calculateAge(birthDate) {
        const today = new Date();
        let age = today.getFullYear() - birthDate.getFullYear();
        const monthDiff = today.getMonth() - birthDate.getMonth();
      
        if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
          age--;
        }
      
        return age;
      }
      
      const birthDate = new Date('1990-05-15');
      console.log(`Age: ${calculateAge(birthDate)}`);

      Timezones and Intl.DateTimeFormat

      Handle timezones and internationalization with the Intl API.

      Timezone Handling
      // Get timezone offset (minutes from UTC)
      const date = new Date();
      const offsetMinutes = date.getTimezoneOffset();
      const offsetHours = -offsetMinutes / 60;
      
      console.log(`Timezone offset: UTC${offsetHours >= 0 ? '+' : ''}${offsetHours}`);
      
      // Intl.DateTimeFormat for timezone-aware formatting
      const formatter = new Intl.DateTimeFormat('en-US', {
        timeZone: 'America/New_York',
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit',
        timeZoneName: 'short'
      });
      
      console.log(formatter.format(date));
      // 'January 15, 2024 at 10:30 AM EST'
      
      // Multiple timezones
      const timezones = ['America/New_York', 'Europe/London', 'Asia/Tokyo'];
      
      timezones.forEach(tz => {
        const formatter = new Intl.DateTimeFormat('en-US', {
          timeZone: tz,
          hour: '2-digit',
          minute: '2-digit',
          timeZoneName: 'short'
        });
      
        console.log(`${tz}: ${formatter.format(date)}`);
      });
      
      // Convert between timezones
      function convertTimezone(date, fromTz, toTz) {
        const dateStr = date.toLocaleString('en-US', { timeZone: fromTz });
        return new Date(dateStr);
      }
      
      // Relative time formatting
      const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
      
      console.log(rtf.format(-1, 'day')); // 'yesterday'
      console.log(rtf.format(1, 'day')); // 'tomorrow'
      console.log(rtf.format(-7, 'day')); // '7 days ago'
      console.log(rtf.format(2, 'week')); // 'in 2 weeks'
      
      // Time ago helper
      function timeAgo(date) {
        const seconds = Math.floor((new Date() - date) / 1000);
      
        const intervals = {
          year: 31536000,
          month: 2592000,
          week: 604800,
          day: 86400,
          hour: 3600,
          minute: 60,
          second: 1
        };
      
        for (const [unit, secondsInUnit] of Object.entries(intervals)) {
          const interval = Math.floor(seconds / secondsInUnit);
      
          if (interval >= 1) {
            return rtf.format(-interval, unit);
          }
        }
      
        return 'just now';
      }
      
      console.log(timeAgo(new Date(Date.now() - 3600000))); // '1 hour ago'
      
      // ISO 8601 with timezone
      function toISOStringWithTimezone(date) {
        const offset = -date.getTimezoneOffset();
        const sign = offset >= 0 ? '+' : '-';
        const hours = Math.floor(Math.abs(offset) / 60).toString().padStart(2, '0');
        const minutes = (Math.abs(offset) % 60).toString().padStart(2, '0');
      
        return date.toISOString().slice(0, -1) + sign + hours + ':' + minutes;
      }
      
      console.log(toISOStringWithTimezone(new Date()));

      Date Libraries: date-fns and Day.js

      Modern libraries provide cleaner APIs and better timezone support than native Date.

      Library Examples
      // date-fns example (import required)
      /*
      import {
        format,
        addDays,
        subMonths,
        differenceInDays,
        isAfter,
        parseISO
      } from 'date-fns';
      
      const now = new Date();
      
      // Formatting
      console.log(format(now, 'yyyy-MM-dd')); // '2024-01-15'
      console.log(format(now, 'MMMM do, yyyy')); // 'January 15th, 2024'
      
      // Manipulation
      const tomorrow = addDays(now, 1);
      const lastMonth = subMonths(now, 1);
      
      // Comparison
      const daysDiff = differenceInDays(tomorrow, now); // 1
      console.log(isAfter(tomorrow, now)); // true
      
      // Parsing
      const date = parseISO('2024-01-15');
      */
      
      // Day.js example (import required)
      /*
      import dayjs from 'dayjs';
      
      const now = dayjs();
      
      // Formatting
      console.log(now.format('YYYY-MM-DD')); // '2024-01-15'
      console.log(now.format('MMMM D, YYYY')); // 'January 15, 2024'
      
      // Manipulation
      const tomorrow = now.add(1, 'day');
      const lastMonth = now.subtract(1, 'month');
      
      // Comparison
      console.log(tomorrow.isAfter(now)); // true
      console.log(now.isBefore(tomorrow)); // true
      
      // Relative time
      console.log(now.from(tomorrow)); // 'in a day'
      console.log(tomorrow.fromNow()); // 'in a day'
      
      // Start/end of periods
      console.log(now.startOf('month'));
      console.log(now.endOf('year'));
      */
      
      // Why use libraries?
      const reasons = [
        'Immutable operations (native Date is mutable)',
        'Better parsing and formatting',
        'Timezone support',
        'More intuitive API',
        'Locale support',
        'Relative time formatting',
        'Duration calculations',
        'Smaller bundle size (especially Day.js: 2KB)'
      ];
      
      // Native Date limitations
      const native = new Date('2024-01-15');
      native.setDate(native.getDate() + 1); // Mutates original
      console.log(native); // Modified
      
      // Libraries are immutable
      // const dayjs1 = dayjs('2024-01-15');
      // const dayjs2 = dayjs1.add(1, 'day');
      // console.log(dayjs1); // Original unchanged
      // console.log(dayjs2); // New instance

      Practice Exercises

      1. Calendar Generator: Build a function that generates a calendar for any month/year
      2. Date Picker Validator: Validate date inputs with min/max dates and disabled dates
      3. Countdown Timer: Create a countdown showing days, hours, minutes, seconds to a future date
      4. Time Tracker: Build a time tracking app that calculates duration between timestamps
      5. Business Days Calculator: Calculate the number of business days between two dates
      6. Recurring Events: Implement logic for daily, weekly, monthly recurring events
      Key Takeaways:
      • Date months are 0-indexed (0=January, 11=December)
      • Use Date.now() or getTime() for timestamps and comparisons
      • toISOString() provides standard format for storage/transmission
      • Intl.DateTimeFormat handles localization and timezones
      • Always validate parsed dates with isNaN() check
      • Consider date-fns or Day.js for production apps (better API, immutable)
      • Be aware of timezone issues when working across regions
      • Store dates as ISO strings or timestamps, not locale-specific formats
      What's Next? Continue your learning journey:

      JSON & Web Storage

      Master data serialization and client-side storage solutions

      Introduction: JavaScript Object Notation (JSON) is the universal format for data exchange in modern web applications. Combined with Web Storage APIs (localStorage and sessionStorage), it enables powerful client-side data persistence. Understanding these technologies is essential for building offline-capable apps, caching data, and improving user experience through persistent state management.

      JSON.parse and JSON.stringify

      Convert between JavaScript objects and JSON strings for data serialization and transmission.

      JSON Serialization Basics
      // Basic JSON.stringify
      const user = {
        id: 123,
        name: 'John Doe',
        email: 'john@example.com',
        active: true
      };
      
      const jsonString = JSON.stringify(user);
      console.log(jsonString);
      // {"id":123,"name":"John Doe","email":"john@example.com","active":true}
      
      // Pretty print with indentation
      const prettyJson = JSON.stringify(user, null, 2);
      console.log(prettyJson);
      // {
      //   "id": 123,
      //   "name": "John Doe",
      //   ...
      // }
      
      // Basic JSON.parse
      const parsedUser = JSON.parse(jsonString);
      console.log(parsedUser.name); // "John Doe"
      
      // Parse with error handling
      function safeJSONParse(str, fallback = null) {
        try {
          return JSON.parse(str);
        } catch (error) {
          console.error('JSON parse error:', error);
          return fallback;
        }
      }
      
      const data = safeJSONParse('invalid json', { default: true });
      
      // Stringify with replacer function
      const obj = {
        name: 'John',
        password: 'secret123',
        age: 30
      };
      
      const filtered = JSON.stringify(obj, (key, value) => {
        if (key === 'password') return undefined; // Exclude password
        return value;
      });
      
      console.log(filtered); // {"name":"John","age":30}
      
      // Stringify with property whitelist
      const whitelisted = JSON.stringify(obj, ['name', 'age']);
      console.log(whitelisted); // {"name":"John","age":30}
      
      // Parse with reviver function
      const jsonWithDate = '{"name":"John","created":"2024-01-15T10:30:00.000Z"}';
      
      const parsed = JSON.parse(jsonWithDate, (key, value) => {
        if (key === 'created') {
          return new Date(value); // Convert to Date object
        }
        return value;
      });
      
      console.log(parsed.created instanceof Date); // true
      
      // Handle special values
      const special = {
        undef: undefined,      // Omitted in JSON
        nul: null,            // Preserved
        nan: NaN,             // Becomes null
        inf: Infinity,        // Becomes null
        func: () => {},       // Omitted
        symbol: Symbol('id'), // Omitted
        date: new Date()      // Becomes string
      };
      
      console.log(JSON.stringify(special));
      // {"nul":null,"nan":null,"inf":null,"date":"2024-01-15T10:30:00.000Z"}

      JSON Error Handling and Validation

      Robust JSON parsing requires proper error handling and validation to prevent application crashes.

      Safe JSON Operations
      // Comprehensive error handling
      function parseJSON(str, options = {}) {
        const {
          fallback = null,
          schema = null,
          onError = console.error
        } = options;
      
        try {
          const parsed = JSON.parse(str);
      
          // Optional schema validation
          if (schema && !validateSchema(parsed, schema)) {
            throw new Error('Schema validation failed');
          }
      
          return parsed;
        } catch (error) {
          onError('JSON parse error:', error);
          return fallback;
        }
      }
      
      // Simple schema validator
      function validateSchema(data, schema) {
        return Object.keys(schema).every(key => {
          const expectedType = schema[key];
          const actualType = typeof data[key];
          return actualType === expectedType;
        });
      }
      
      // Usage
      const userSchema = {
        id: 'number',
        name: 'string',
        email: 'string'
      };
      
      const userData = parseJSON('{"id":123,"name":"John","email":"john@example.com"}', {
        schema: userSchema,
        fallback: { id: 0, name: 'Guest', email: '' }
      });
      
      // Safe stringify with circular reference handling
      function safeStringify(obj, space = 0) {
        const seen = new WeakSet();
      
        return JSON.stringify(obj, (key, value) => {
          if (typeof value === 'object' && value !== null) {
            if (seen.has(value)) {
              return '[Circular]';
            }
            seen.add(value);
          }
          return value;
        }, space);
      }
      
      // Test circular reference
      const circular = { name: 'Test' };
      circular.self = circular;
      
      console.log(safeStringify(circular, 2));
      // {
      //   "name": "Test",
      //   "self": "[Circular]"
      // }
      
      // Validate JSON string before parsing
      function isValidJSON(str) {
        try {
          JSON.parse(str);
          return true;
        } catch {
          return false;
        }
      }
      
      if (isValidJSON(userInput)) {
        const data = JSON.parse(userInput);
        processData(data);
      }
      
      // Deep clone with JSON (simple objects only)
      function deepClone(obj) {
        try {
          return JSON.parse(JSON.stringify(obj));
        } catch (error) {
          console.error('Clone failed:', error);
          return null;
        }
      }
      
      const original = { a: 1, b: { c: 2 } };
      const clone = deepClone(original);
      clone.b.c = 3;
      console.log(original.b.c); // Still 2
      
      // Custom toJSON method
      class User {
        constructor(name, password) {
          this.name = name;
          this._password = password;
        }
      
        toJSON() {
          return {
            name: this.name,
            // Exclude password from serialization
          };
        }
      }
      
      const user = new User('John', 'secret');
      console.log(JSON.stringify(user)); // {"name":"John"}

      localStorage API

      localStorage persists data across browser sessions with no expiration - perfect for user preferences and app state.

      localStorage Operations
      // Basic operations
      localStorage.setItem('username', 'John Doe');
      const username = localStorage.getItem('username');
      localStorage.removeItem('username');
      localStorage.clear(); // Remove all items
      
      // Store objects (must stringify)
      const settings = {
        theme: 'dark',
        language: 'en',
        notifications: true
      };
      
      localStorage.setItem('settings', JSON.stringify(settings));
      
      // Retrieve objects (must parse)
      const savedSettings = JSON.parse(
        localStorage.getItem('settings') || '{}'
      );
      
      // Safe storage wrapper
      const storage = {
        set(key, value) {
          try {
            const serialized = JSON.stringify(value);
            localStorage.setItem(key, serialized);
            return true;
          } catch (error) {
            console.error('Storage error:', error);
            return false;
          }
        },
      
        get(key, fallback = null) {
          try {
            const item = localStorage.getItem(key);
            return item ? JSON.parse(item) : fallback;
          } catch (error) {
            console.error('Retrieval error:', error);
            return fallback;
          }
        },
      
        remove(key) {
          localStorage.removeItem(key);
        },
      
        clear() {
          localStorage.clear();
        },
      
        has(key) {
          return localStorage.getItem(key) !== null;
        }
      };
      
      // Usage
      storage.set('user', { id: 123, name: 'John' });
      const user = storage.get('user', { id: 0, name: 'Guest' });
      
      // Get all keys
      function getAllKeys() {
        return Object.keys(localStorage);
      }
      
      // Get all items
      function getAllItems() {
        const items = {};
        for (let i = 0; i < localStorage.length; i++) {
          const key = localStorage.key(i);
          items[key] = storage.get(key);
        }
        return items;
      }
      
      // Namespace pattern to avoid conflicts
      class NamespacedStorage {
        constructor(namespace) {
          this.namespace = namespace;
        }
      
        _key(key) {
          return `${this.namespace}:${key}`;
        }
      
        set(key, value) {
          storage.set(this._key(key), value);
        }
      
        get(key, fallback) {
          return storage.get(this._key(key), fallback);
        }
      
        remove(key) {
          storage.remove(this._key(key));
        }
      
        clear() {
          const keys = Object.keys(localStorage);
          keys.forEach(key => {
            if (key.startsWith(`${this.namespace}:`)) {
              localStorage.removeItem(key);
            }
          });
        }
      }
      
      // Usage
      const appStorage = new NamespacedStorage('myapp');
      appStorage.set('user', { name: 'John' });
      appStorage.set('settings', { theme: 'dark' });
      
      // Expiring storage
      class ExpiringStorage {
        set(key, value, ttl) {
          const item = {
            value,
            expiry: Date.now() + ttl
          };
          storage.set(key, item);
        }
      
        get(key, fallback = null) {
          const item = storage.get(key);
      
          if (!item) return fallback;
      
          if (Date.now() > item.expiry) {
            storage.remove(key);
            return fallback;
          }
      
          return item.value;
        }
      }
      
      const expiring = new ExpiringStorage();
      expiring.set('temp', 'data', 60000); // Expires in 1 minute

      sessionStorage API

      sessionStorage persists data only for the browser session - perfect for temporary state and form data.

      sessionStorage Usage
      // Same API as localStorage but session-scoped
      sessionStorage.setItem('tempData', 'value');
      const tempData = sessionStorage.getItem('tempData');
      sessionStorage.removeItem('tempData');
      sessionStorage.clear();
      
      // Use cases for sessionStorage
      // 1. Form data preservation during navigation
      function saveFormData(formId) {
        const form = document.querySelector(`#${formId}`);
        const formData = new FormData(form);
        const data = Object.fromEntries(formData);
      
        sessionStorage.setItem(`form_${formId}`, JSON.stringify(data));
      }
      
      function restoreFormData(formId) {
        const saved = sessionStorage.getItem(`form_${formId}`);
        if (!saved) return;
      
        const data = JSON.parse(saved);
        const form = document.querySelector(`#${formId}`);
      
        Object.keys(data).forEach(key => {
          const input = form.elements[key];
          if (input) input.value = data[key];
        });
      }
      
      // Auto-save form on input
      document.querySelector('#myForm')?.addEventListener('input', () => {
        saveFormData('myForm');
      });
      
      // Restore on page load
      window.addEventListener('DOMContentLoaded', () => {
        restoreFormData('myForm');
      });
      
      // 2. Multi-step form state
      class MultiStepForm {
        constructor(formId) {
          this.formId = formId;
          this.storageKey = `multistep_${formId}`;
        }
      
        saveStep(step, data) {
          const formState = this.getState();
          formState[step] = data;
          sessionStorage.setItem(this.storageKey, JSON.stringify(formState));
        }
      
        getStep(step) {
          const formState = this.getState();
          return formState[step] || {};
        }
      
        getState() {
          const saved = sessionStorage.getItem(this.storageKey);
          return saved ? JSON.parse(saved) : {};
        }
      
        clear() {
          sessionStorage.removeItem(this.storageKey);
        }
      }
      
      // Usage
      const form = new MultiStepForm('registration');
      form.saveStep('personal', { name: 'John', age: 30 });
      form.saveStep('contact', { email: 'john@example.com' });
      
      // 3. Tab-specific state
      sessionStorage.setItem('currentTab', 'dashboard');
      
      // localStorage vs sessionStorage comparison
      const comparison = {
        localStorage: {
          scope: 'All tabs/windows',
          persistence: 'Permanent',
          useCase: 'Settings, preferences, cache'
        },
        sessionStorage: {
          scope: 'Single tab',
          persistence: 'Until tab closes',
          useCase: 'Form data, temporary state'
        }
      };

      Storage Limits and Events

      Understand storage capacity limits and respond to storage changes across tabs.

      Storage Limits and Events
      // Check available space (approximate)
      function getStorageSize() {
        let total = 0;
        for (let key in localStorage) {
          if (localStorage.hasOwnProperty(key)) {
            total += localStorage[key].length + key.length;
          }
        }
        return (total / 1024).toFixed(2) + ' KB';
      }
      
      console.log('Storage used:', getStorageSize());
      
      // Test storage limit
      function testStorageLimit() {
        const testKey = 'sizeTest';
        let size = 0;
      
        try {
          // Typical limit: 5-10MB
          for (let i = 0; i < 1024; i++) {
            localStorage.setItem(testKey, 'x'.repeat(1024 * i));
            size = i;
          }
        } catch (e) {
          console.log('Storage limit reached at:', size, 'KB');
          localStorage.removeItem(testKey);
        }
      }
      
      // Handle quota exceeded error
      function safeSetItem(key, value) {
        try {
          localStorage.setItem(key, value);
          return true;
        } catch (e) {
          if (e.name === 'QuotaExceededError') {
            console.warn('Storage quota exceeded');
            // Strategy: Clear old data
            clearOldData();
      
            // Try again
            try {
              localStorage.setItem(key, value);
              return true;
            } catch {
              return false;
            }
          }
          return false;
        }
      }
      
      function clearOldData() {
        // Remove items with oldest timestamp
        const items = [];
      
        for (let i = 0; i < localStorage.length; i++) {
          const key = localStorage.key(i);
          try {
            const item = JSON.parse(localStorage.getItem(key));
            if (item.timestamp) {
              items.push({ key, timestamp: item.timestamp });
            }
          } catch {}
        }
      
        items.sort((a, b) => a.timestamp - b.timestamp);
      
        // Remove oldest 20%
        const toRemove = Math.ceil(items.length * 0.2);
        items.slice(0, toRemove).forEach(item => {
          localStorage.removeItem(item.key);
        });
      }
      
      // Storage event - sync across tabs
      window.addEventListener('storage', (e) => {
        console.log('Storage changed:');
        console.log('Key:', e.key);
        console.log('Old value:', e.oldValue);
        console.log('New value:', e.newValue);
        console.log('URL:', e.url);
      
        // React to changes
        if (e.key === 'theme') {
          applyTheme(e.newValue);
        }
      
        if (e.key === 'logout') {
          window.location.href = '/login';
        }
      });
      
      // Broadcast changes across tabs
      function broadcastLogout() {
        localStorage.setItem('logout', Date.now().toString());
        localStorage.removeItem('logout'); // Triggers storage event
      }
      
      // Sync state across tabs
      class CrossTabSync {
        constructor(key) {
          this.key = key;
          this.listeners = [];
      
          window.addEventListener('storage', (e) => {
            if (e.key === this.key && e.newValue) {
              const data = JSON.parse(e.newValue);
              this.listeners.forEach(fn => fn(data));
            }
          });
        }
      
        set(data) {
          localStorage.setItem(this.key, JSON.stringify(data));
        }
      
        get() {
          const item = localStorage.getItem(this.key);
          return item ? JSON.parse(item) : null;
        }
      
        onChange(callback) {
          this.listeners.push(callback);
        }
      }
      
      // Usage
      const userSync = new CrossTabSync('currentUser');
      userSync.onChange((user) => {
        console.log('User updated in another tab:', user);
        updateUI(user);
      });

      Cookies Basics

      Cookies are small text files sent with HTTP requests - useful for authentication and tracking.

      Cookie Management
      // Set cookie
      document.cookie = 'username=John; max-age=3600; path=/';
      
      // Cookie with expiration date
      const expires = new Date();
      expires.setDate(expires.getDate() + 7); // 7 days
      document.cookie = `token=abc123; expires=${expires.toUTCString()}; path=/`;
      
      // Secure and HttpOnly cookies (requires server)
      document.cookie = 'session=xyz; secure; samesite=strict';
      
      // Get cookie value
      function getCookie(name) {
        const matches = document.cookie.match(
          new RegExp('(?:^|; )' + name + '=([^;]*)')
        );
        return matches ? decodeURIComponent(matches[1]) : null;
      }
      
      const username = getCookie('username');
      
      // Get all cookies as object
      function getAllCookies() {
        return document.cookie.split('; ').reduce((acc, cookie) => {
          const [key, value] = cookie.split('=');
          acc[key] = decodeURIComponent(value);
          return acc;
        }, {});
      }
      
      // Delete cookie
      function deleteCookie(name) {
        document.cookie = `${name}=; max-age=0; path=/`;
      }
      
      // Cookie helper class
      class CookieManager {
        static set(name, value, days = 7, options = {}) {
          const { path = '/', secure = false, sameSite = 'lax' } = options;
      
          const expires = new Date();
          expires.setDate(expires.getDate() + days);
      
          let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
          cookie += `; expires=${expires.toUTCString()}`;
          cookie += `; path=${path}`;
      
          if (secure) cookie += '; secure';
          cookie += `; samesite=${sameSite}`;
      
          document.cookie = cookie;
        }
      
        static get(name) {
          return getCookie(name);
        }
      
        static delete(name) {
          deleteCookie(name);
        }
      
        static has(name) {
          return getCookie(name) !== null;
        }
      }
      
      // Usage
      CookieManager.set('preferences', JSON.stringify({ theme: 'dark' }), 30);
      const prefs = JSON.parse(CookieManager.get('preferences') || '{}');
      
      // Cookies vs Storage
      const storageComparison = {
        cookies: {
          size: '4KB',
          sentToServer: true,
          expiration: 'Configurable',
          useCase: 'Authentication, tracking'
        },
        localStorage: {
          size: '5-10MB',
          sentToServer: false,
          expiration: 'Never',
          useCase: 'App state, cache'
        },
        sessionStorage: {
          size: '5-10MB',
          sentToServer: false,
          expiration: 'Session',
          useCase: 'Temporary state'
        }
      };

      IndexedDB Introduction

      IndexedDB is a low-level API for storing large amounts of structured data in the browser.

      IndexedDB Basics
      // Open database
      function openDB(name, version = 1) {
        return new Promise((resolve, reject) => {
          const request = indexedDB.open(name, version);
      
          request.onerror = () => reject(request.error);
          request.onsuccess = () => resolve(request.result);
      
          request.onupgradeneeded = (e) => {
            const db = e.target.result;
      
            // Create object store (table)
            if (!db.objectStoreNames.contains('users')) {
              const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
              store.createIndex('email', 'email', { unique: true });
              store.createIndex('name', 'name', { unique: false });
            }
          };
        });
      }
      
      // Add data
      async function addUser(user) {
        const db = await openDB('myapp');
        const transaction = db.transaction(['users'], 'readwrite');
        const store = transaction.objectStore('users');
      
        return new Promise((resolve, reject) => {
          const request = store.add(user);
          request.onsuccess = () => resolve(request.result);
          request.onerror = () => reject(request.error);
        });
      }
      
      // Get data
      async function getUser(id) {
        const db = await openDB('myapp');
        const transaction = db.transaction(['users'], 'readonly');
        const store = transaction.objectStore('users');
      
        return new Promise((resolve, reject) => {
          const request = store.get(id);
          request.onsuccess = () => resolve(request.result);
          request.onerror = () => reject(request.error);
        });
      }
      
      // Get all data
      async function getAllUsers() {
        const db = await openDB('myapp');
        const transaction = db.transaction(['users'], 'readonly');
        const store = transaction.objectStore('users');
      
        return new Promise((resolve, reject) => {
          const request = store.getAll();
          request.onsuccess = () => resolve(request.result);
          request.onerror = () => reject(request.error);
        });
      }
      
      // Usage
      await addUser({ name: 'John', email: 'john@example.com' });
      const user = await getUser(1);
      const allUsers = await getAllUsers();
      
      // When to use IndexedDB
      const indexedDBUseCases = [
        'Offline-first applications',
        'Large datasets (> 10MB)',
        'Complex queries with indexes',
        'Binary data (files, images)',
        'Progressive Web Apps (PWAs)'
      ];

      Data Validation Patterns

      Validate data before storing and after retrieving to ensure data integrity.

      Storage Validation
      // Schema validator
      class StorageValidator {
        constructor(schema) {
          this.schema = schema;
        }
      
        validate(data) {
          const errors = [];
      
          Object.keys(this.schema).forEach(key => {
            const rules = this.schema[key];
            const value = data[key];
      
            if (rules.required && value === undefined) {
              errors.push(`${key} is required`);
            }
      
            if (value !== undefined && rules.type && typeof value !== rules.type) {
              errors.push(`${key} must be ${rules.type}`);
            }
      
            if (rules.min !== undefined && value < rules.min) {
              errors.push(`${key} must be >= ${rules.min}`);
            }
      
            if (rules.max !== undefined && value > rules.max) {
              errors.push(`${key} must be <= ${rules.max}`);
            }
      
            if (rules.pattern && !rules.pattern.test(value)) {
              errors.push(`${key} format is invalid`);
            }
          });
      
          return {
            valid: errors.length === 0,
            errors
          };
        }
      }
      
      // Usage
      const userSchema = new StorageValidator({
        id: { required: true, type: 'number', min: 1 },
        name: { required: true, type: 'string' },
        email: {
          required: true,
          type: 'string',
          pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
        },
        age: { type: 'number', min: 0, max: 150 }
      });
      
      function saveUser(user) {
        const validation = userSchema.validate(user);
      
        if (!validation.valid) {
          console.error('Validation errors:', validation.errors);
          return false;
        }
      
        storage.set('user', user);
        return true;
      }
      
      // Versioned storage with migration
      class VersionedStorage {
        constructor(key, version, migrations) {
          this.key = key;
          this.version = version;
          this.migrations = migrations;
        }
      
        get() {
          const stored = storage.get(this.key);
      
          if (!stored) return null;
      
          let data = stored.data;
          let version = stored.version || 1;
      
          // Run migrations
          while (version < this.version) {
            const migration = this.migrations[version];
            if (migration) {
              data = migration(data);
            }
            version++;
          }
      
          return data;
        }
      
        set(data) {
          storage.set(this.key, {
            version: this.version,
            data
          });
        }
      }
      
      // Usage with migrations
      const userStorage = new VersionedStorage('user', 3, {
        2: (data) => ({ ...data, createdAt: Date.now() }),
        3: (data) => ({ ...data, settings: { theme: 'light' } })
      });
      
      userStorage.set({ name: 'John', email: 'john@example.com' });
      const user = userStorage.get(); // Has version 3 structure

      Practice Exercises

      1. Settings Manager: Build a settings component that persists user preferences with localStorage
      2. Shopping Cart: Create a shopping cart that persists items across sessions
      3. Form Auto-Save: Implement form data preservation with sessionStorage during page navigation
      4. Multi-Tab Sync: Build a notification system that syncs across browser tabs using storage events
      5. Offline Cache: Create a cache manager that stores API responses with TTL in localStorage
      6. Storage Inspector: Build a tool that displays all storage items, their size, and allows clearing
      Key Takeaways:
      • JSON.stringify/parse convert between objects and strings - always handle errors
      • localStorage persists across sessions; sessionStorage only for current tab
      • Storage limit is typically 5-10MB - handle QuotaExceededError gracefully
      • Always parse localStorage data with try/catch to prevent crashes
      • Use namespaces to avoid key conflicts in shared storage
      • Storage events enable cross-tab communication
      • Cookies are sent with HTTP requests - use for authentication, not bulk data
      • IndexedDB for large datasets and complex queries; localStorage for simple data
      What's Next? Continue your learning journey:

      Best Practices

      • Prefer const, then let; avoid var
      • Keep functions small and pure where possible
      • Handle errors and unexpected states explicitly
      • Lint and format for consistency

      Testing and Linting

      Automate quality with Jest or Vitest for unit tests, and ESLint + Prettier for consistent style.

      Sample Test
      // sum.test.js
      import { sum } from './sum';
      
      test('adds numbers', () => {
        expect(sum(2, 3)).toBe(5);
      });
      

      Last updated: February 2026