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.
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.
// 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.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.
Waiting...
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.
// Paste into the console to experiment without editing files
const el = document.querySelector('#status');
el.style.color = '#0a8754';
el.textContent = 'Live edit success!';
// 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.
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.
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.
# Verify Node and npm
node -v
npm -v
# Run a simple script
node 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.
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
- Create a Starter Page: Build a minimal HTML page with a deferred external script and verify it updates a paragraph.
- Console Reporter: Log grouped console messages for a mock sign-in flow and include timing with
console.time. - Click Counter: Add a button that increments a visible counter and logs the total to the console.
- Fetch and Render: Fetch JSON from a public API and render one field into the DOM with error handling.
- Node Hello: Write a Node script that reads a local file and prints its contents, handling missing-file errors.
- Debug Session: Set a breakpoint in a simple function, step through it, and note each variable value.
- Use clean HTML shells and the
deferattribute 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.
<!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.
// 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.
// 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.
/* 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.
// 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.
// 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
- Console Exploration: Open browser DevTools (F12), go to Console tab, and experiment with basic JavaScript expressions.
- First Script: Create an HTML file with a button that displays an alert when clicked.
- DOM Interaction: Change the text content and style of an HTML element using JavaScript.
- Research: Explore the npm registry (npmjs.com) and identify 5 popular JavaScript packages.
- Environment Check: Investigate what JavaScript engine your browser uses (V8, SpiderMonkey, JavaScriptCore).
- 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.
/* 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.
/* 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.
/* 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/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.
/* 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.
/* 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
- Research: Read about Brendan Eich's 10-day creation of JavaScript and the compromises made.
- Timeline: Create a visual timeline of JavaScript's major releases and their features.
- Code Evolution: Take an ES5 code sample and refactor it using ES6+ features.
- Browser Compatibility: Use caniuse.com to check ES6 feature support across browsers.
- Ecosystem Exploration: Explore npm trends and identify the most downloaded packages.
- 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.
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);
}
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.
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'));
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.
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
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.
// 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}`);
}
/**
* 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.
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)
// 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.
'use strict';
function assign() {
// next line would throw ReferenceError instead of creating a global
// oops = 123;
return true;
}
assign();
// 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.
// 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;
}
// 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.
// 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}`;
}
// .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
- Rewrite three expressions as statements and three statements as expressions; note where each is valid.
- Demonstrate an ASI pitfall with
returnand fix it with explicit semicolons. - Create a small module with two exported functions and import it into another file.
- Use strict mode to catch an accidental implicit global and document the error message.
- Write an example that shows how
letblock scope differs fromvarin a loop. - Add meaningful comments to a function explaining why a decision was made, then remove any redundant comments.
- List five reserved keywords and show a valid identifier alternative for each.
- Configure a simple ESLint rule set and run it on a file to see reported syntax/style issues.
- Format a multi-line expression with parentheses to make operator precedence explicit.
- Organize a file by grouping imports, constants, functions, and exports in a consistent order.
- Distinguish statements from expressions to place code in the right contexts.
- Use blocks to manage scope, and rely on
let/constrather thanvar. - Write explicit semicolons to avoid ASI edge cases, especially after
returnand 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.
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.
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.
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.
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.
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.
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.
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.
// 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.
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.
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.
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.
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
- Refactor Legacy: Take a snippet that uses
varand convert it tolet/const, confirming behavior stays the same. - Scope Guard: Create a loop that declares a block-scoped variable and verify it is not accessible outside the block.
- Immutable Config: Define a configuration object with
constand update one of its properties safely. - TDZ Experiment: Intentionally access a
letbinding before its declaration to observe the ReferenceError, then fix the order. - Swap Values: Use array destructuring with
letto swap two variables without a temporary placeholder. - Naming Audit: Rename ambiguous variables in a small function to communicate intent clearly.
- Prefer
constby default; reach forletonly when reassignment is required. letandconstare block-scoped, preventing accidental leaks.varis function-scoped and hoisted; refactor it out in modern code.- The temporal dead zone enforces ordering and reduces undefined access bugs.
constprotects the binding, not the contents of objects or arrays.- Readable, intention-revealing names make state changes obvious.
- Destructuring works cleanly with
constandletfor 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.
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.
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.
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.”
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.
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().
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.
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.
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.
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.
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.
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));
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
- Type Audit: Log the
typeofresult for every field in an object that mixes strings, numbers, booleans, and arrays. - Coercion Catch: Write comparisons using
==and rewrite them with===, noting behavioral changes. - BigInt Limit: Find a number larger than
Number.MAX_SAFE_INTEGERand represent it safely with BigInt. - Clone and Mutate: Clone an object, mutate the clone, and verify the original is unchanged.
- Null Handling: Build a function that returns
nullwhen data is intentionally empty andundefinedwhen a property is missing. - Precision Fix: Create a helper that rounds currency totals to two decimals to avoid floating-point artifacts.
- JavaScript primitives are string, number, boolean, null, undefined, symbol, and bigint.
- Objects and arrays are reference types; assignments copy references, not values.
typeofworks for primitives but needs helpers for arrays andnull.- Watch out for NaN, Infinity, and floating-point precision issues in calculations.
- Use strict equality and explicit casts to avoid unwanted coercion.
nullsignals intentional emptiness;undefinedmeans 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.
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 });
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.
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
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.
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 });
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.
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'
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.
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 });
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.
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 });
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.
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 });
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.
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
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.
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 });
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
- Calculate a shopping cart total with tax, discount, and shipping using arithmetic and compound assignments.
- Write a guard clause function that returns early if a user is missing an email or has not accepted terms.
- Create a permissions bitmask for read/write/delete and check each permission with bitwise operators.
- Use optional chaining and nullish coalescing to safely read nested API response data.
- Implement a ternary-based grade calculator that outputs A/B/C/F based on numeric ranges.
- Build a
sum(...numbers)function with rest parameters and test it with different lengths. - Demonstrate
typeofchecks for primitives andinstanceofchecks for custom classes. - Refactor a complex logical expression by adding parentheses to make precedence explicit.
- Spread an array of scores into a new array and append two extra scores without mutating the original.
- Explore short-circuiting by logging which functions run inside
a && b()anda || b().
- 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.
typeoftargets primitives, whileinstanceofchecks 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.
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));
// 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.
const points = 120;
const tier = points > 200 ? 'gold' : points > 100 ? 'silver' : 'bronze';
const label = points >= 100 ? 'Qualified' : 'Pending';
console.log({ tier, label });
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.
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'));
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.
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)
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.
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 });
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.
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
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.
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 }));
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
- Rewrite a nested
ifchain using guard clauses to flatten the structure. - Implement a
switchthat groups multiple cases together and includes a default. - Create a function that distinguishes between
0,null, andundefinedusing nullish coalescing. - Show how a ternary can replace a simple
ifand when it should not. - List all falsy values in JavaScript and test each in an
ifstatement. - Wrap a JSON parsing call in
try/catchand log a friendly error message. - Demonstrate
finallyby releasing a resource (e.g., clearing a timer) regardless of success. - Use optional chaining to safely access a deep property on an object from an API.
- Build a permission check that returns early when the user is missing required roles.
- Combine logical operators with parentheses to make precedence clear in a complex condition.
- Flatten control flow with guard clauses and early returns to keep happy paths visible.
- Use
switchfor 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/finallyto 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.
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);
}
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.
let attempts = 0;
while (attempts < 3) {
console.log('Attempt', attempts + 1);
attempts++;
}
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.
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);
}
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.
const user = { name: 'Lee', role: 'editor', active: true };
for (const key in user) {
if (Object.hasOwn(user, key)) {
console.log(key, user[key]);
}
}
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.
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
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.
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 });
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.
const totals = [5, 10, 15];
const sum = totals.reduce((acc, value) => acc + value, 0);
console.log(sum); // 30
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.
for (let i = 1; i <= 5; i++) {
if (i % 2 === 0) continue; // skip even numbers
console.log('Odd', i);
}
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
- Write a classic
forloop that counts down by 3s and stops at zero. - Use
for...ofto iterate a string and collect vowel counts. - Iterate an object with
for...inwhile guarding withObject.hasOwn. - Build a list of objects using
forEachand note whybreakwill not work. - Create a transformation pipeline with
map,filter, andreduceto compute totals. - Demonstrate
do...whileby prompting until valid input arrives. - Find the first item that matches a predicate using a loop with
breakand compare tofind. - Show how
continueskips specific iterations in a validation loop. - Use a labeled
breakto exit nested loops when a target cell is found. - Measure readability by rewriting a
forloop into array helpers and note trade-offs.
- Pick the loop that matches your data:
forfor indices,for...offor iterables,for...infor object keys. - Array helpers like
map,filter, andreduceencourage declarative, side-effect-free transformations. breakandcontinueshape control inside loops; labels should be rare and intentional.whileanddo...whilesuit 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.
function greet(name) {
return `Hello, ${name}`;
}
console.log(greet('Mia'));
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.
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' }));
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.
const settings = (() => {
const secret = 'token-123';
const baseUrl = 'https://api.example.com';
return { baseUrl, getToken: () => secret };
})();
console.log(settings.getToken());
(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.
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 }));
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.
function divide(a, b) {
if (b === 0) return null;
return a / b;
}
console.log(divide(10, 2));
console.log(divide(10, 0));
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.
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
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.
function makeCounter() {
let count = 0;
return () => {
count++;
return count;
};
}
const next = makeCounter();
console.log(next());
console.log(next());
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.
function applyTwice(fn, value) {
return fn(fn(value));
}
const increment = n => n + 1;
console.log(applyTwice(increment, 3)); // 5
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
- Create a function declaration and a function expression that both greet a user; log their outputs.
- Write an arrow function that returns an object literal and test lexical
thisinside a method. - Build an IIFE that initializes configuration and exposes only a getter.
- Implement a function with default parameters and rest parameters, then destructure its arguments.
- Write a recursive function to sum nested arrays (e.g.,
[1, [2, [3]]]). - Create a closure-based counter with increment and reset capabilities.
- Return a function from another function that multiplies numbers by a chosen factor.
- Demonstrate early returns for invalid input in a form validation function.
- Use higher-order functions to apply a callback to a dataset and compare to inline logic.
- Document a function with JSDoc including parameter types and return type.
- Functions are first-class: declare, assign, pass, and return them freely.
- Arrow functions are concise and capture
thislexically; 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.
var greeting = 'hello';
function run() {
var greeting = 'hi';
console.log(greeting);
}
run();
console.log(greeting);
function setFlag() {
flag = true; // implicit global (avoid)
}
setFlag();
console.log(window.flag);
Block Scope
let and const respect block boundaries like if and for.
if (true) {
let scoped = 'inside';
const fixed = 42;
}
// console.log(scoped); // ReferenceError
// console.log(fixed); // ReferenceError
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.
const outer = 'outside';
function parent() {
const mid = 'middle';
function child() {
const inner = 'inside';
console.log(outer, mid, inner);
}
child();
}
parent();
function parent() {
const secret = 'hidden';
}
// console.log(secret); // ReferenceError
Hoisting Rules
Declarations are hoisted to the top of their scope. Initializations are not.
console.log(total); // undefined due to hoisting
var total = 10;
// Behind the scenes:
// var total;
// console.log(total);
// total = 10;
// 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.
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.
run();
function run() {
console.log('runs');
}
// 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.
function makeCounter() {
let value = 0;
return function() {
value++;
return value;
};
}
const inc = makeCounter();
console.log(inc());
console.log(inc());
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.
'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.
let value = 1;
function sample() {
console.log(value);
let value = 2; // TDZ hides outer value
}
// sample(); // ReferenceError
function configure() {
var mode = 'a';
var mode = 'b'; // allowed but risky
return mode;
}
console.log(configure());
Practice Exercises
- Demonstrate the difference between
varandletinside aforloop withsetTimeout. - Write a function that logs a variable before and after declaration using
var, then repeat withletand compare errors. - Create nested functions that show lexical scope access from inner to outer variables.
- Build a closure-based counter that exposes increment and reset; verify outer variables stay private.
- Enable strict mode in a function and attempt to assign to an undeclared variable to see the thrown error.
- Refactor a function that accidentally relies on hoisted undefined values into a safer version.
- Illustrate the temporal dead zone by accessing a
letbinding before declaration inside a block. - Use destructuring with defaults inside a function parameter and observe how scope resolution works.
- Explain why function declarations are hoisted but function expressions may fail before initialization.
- Create a module-level constant and show how shadowing it inside a block changes only the inner scope.
varis function-scoped;let/constare block-scoped and live in the temporal dead zone until initialized.- Hoisting moves declarations, not assignments; accessing too early yields
undefinedor 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.
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 }
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.
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);
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.
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
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.
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
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.
const scores = [80, 92, 67];
const curved = scores.map(s => s + 5);
const passed = scores.filter(s => s >= 70);
console.log({ curved, passed });
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.
const flags = [true, true, false];
console.log(flags.some(Boolean)); // true
console.log(flags.every(Boolean)); // false
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.
const values = [10, 2, 30, 25];
const sorted = [...values].sort((a, b) => a - b);
const reversed = [...sorted].reverse();
console.log({ values, sorted, reversed });
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.
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']
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.
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);
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
- Create arrays using literals, constructors, and spread from a Set.
- Use
push/pop/shift/unshiftto manage a queue and log its state. - Experiment with
spliceto insert, replace, and remove items at different positions. - Slice an array into segments and recombine them with
concator spread. - Transform a list with
mapand filter it withfilter; compute totals withreduce. - Find an object in an array with
findand its position withfindIndex. - Sort numbers and strings; note when you need a comparator for correct numeric order.
- Flatten nested arrays with
flatand merge mapping plus flattening withflatMap. - Clone an array deeply enough to modify nested arrays without touching the original.
- Build a pipeline that chains
filter,map, andreduceto summarize data.
- Arrays are ordered collections; access by index and track size with
length. - Use mutating methods (
push,pop,splice) cautiously; prefer non-mutating copies withslice,concat, or spread. - Transform data declaratively with
map,filter,reduce, and search withfind,some,every. sortandreversemutate; copy first if you need the original order.flatandflatMapsimplify 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.
const user = {
name: 'Alex',
age: 28,
role: 'editor',
active: true
};
console.log(user.name);
console.log(user['role']);
user.location = 'Remote';
user['theme'] = 'dark';
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.
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));
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.
const title = 'Engineer';
const level = 'Senior';
const profile = { title, level, location: 'NYC' };
console.log(profile);
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.
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.
const base = { ready: true, retries: 0 };
const copyA = Object.assign({}, base, { retries: 1 });
const copyB = { ...base, retries: 2 };
console.log(copyA, copyB);
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.
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.
const customer = { id: 7, name: 'Sam', plan: 'pro' };
const { name, plan } = customer;
console.log(name, plan);
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.
const env = Object.freeze({ mode: 'prod', version: '1.0.0' });
env.mode = 'dev';
console.log(env.mode); // still prod
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.
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());
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
- Create an object with shorthand properties and add a computed property for today's date.
- Write a method that uses
thisto update a balance; test what happens when the method is detached. - Clone an object with spread, change a nested property safely, and compare to the original.
- Use
Object.entriesto convert an object to query-string format. - Destructure an object with renaming and defaults, then use the rest property to collect remaining fields.
- Freeze a configuration object and attempt to mutate it; observe the result.
- Build a factory function that returns multiple counters sharing independent private state.
- Compose two capability mixins into a single object and verify both behaviors work.
- Write a helper that accepts an object of feature flags and returns only the enabled ones.
- Experiment with optional chaining on a nested object where an intermediate property is missing.
- Objects store key-value pairs, including methods that use
thisfor context. - Shorthand and computed properties keep object literals concise and dynamic.
- Spread and
Object.assigncreate shallow copies; avoid mutating originals. Object.keys,Object.values, andObject.entrieshelp 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.
const colors = ['red', 'green', 'blue'];
const [primary, secondary, tertiary] = colors;
console.log(primary, secondary, tertiary);
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.
let left = 'A';
let right = 'B';
[left, right] = [right, left];
console.log(left, right);
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.
const user = { id: 9, name: 'Kai', role: 'admin' };
const { name, role: jobTitle } = user;
console.log(name, jobTitle);
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.
const [first, ...others] = ['ui', 'api', 'db', 'ops'];
console.log(first);
console.log(others);
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.
const baseStack = ['HTML', 'CSS'];
const stack = [...baseStack, 'JavaScript', 'TypeScript'];
const copy = [...stack];
console.log(stack);
console.log(copy);
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.
function createUser({ name, role = 'viewer', active = true }) {
return { name, role, active };
}
console.log(createUser({ name: 'Mira' }));
function logTopTwo([first, second]) {
console.log('Top:', first, second);
}
logTopTwo(['alpha', 'beta', 'gamma']);
Practical Transformations
Combine destructuring, spread, and rest for expressive data manipulation.
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);
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.
const config = null;
const { mode = 'prod', retries = 1 } = config ?? {};
console.log(mode, retries);
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.
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.
const entries = [
['theme', 'dark'],
['lang', 'en']
];
for (const [key, value] of entries) {
console.log(key, value);
}
function renderUser({ profile: { name }, stats: { followers = 0 } = {} }) {
return `${name} (${followers} followers)`;
}
console.log(renderUser({ profile: { name: 'Mona' }, stats: { followers: 10 } }));
Practice Exercises
- Destructure the first two items of an array and gather the rest; log all three results.
- Swap two variables using array destructuring without creating a third variable.
- Destructure a nested object that may be missing keys using defaults and optional chaining.
- Use spread to clone an array and insert an item in the middle position.
- Write a function that destructures its object parameter and sets defaults for missing options.
- Combine two objects with spread, then override a nested field safely without mutating sources.
- Convert an array of entries back into an object after destructuring each pair.
- Demonstrate that object spread is shallow by mutating a nested value and observing both objects.
- Use rest parameters to capture extra arguments from a function call.
- Extract the first and last elements of an array while gathering the middle items into another array.
- 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
Multi-line Strings
Create strings that span multiple lines without escape characters or concatenation.
// 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.
// 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.
// 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 => `${col.label} `).join('')}
${data.map(row => `
${columns.map(col => `${row[col.key]} `).join('')}
`).join('')}
`;
}
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.
// 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.
// 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.
// 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.
// 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}
`;
}
function Page(title, body) {
return Layout(title, `
`);
}
// 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 ',
''
);
Practice Exercises
- Email Template: Create a tagged template for generating HTML emails with styling
- Markdown Renderer: Build a simple markdown-to-HTML converter using templates
- Form Generator: Create a function that generates HTML forms from configuration objects
- Table Builder: Build a reusable table component with sorting and filtering
- Safe HTML Tag: Implement a tagged template that automatically escapes user input
- CSS-in-JS: Create a styled components-like system using tagged templates
- 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
- Regular Expressions - Advanced string manipulation
- DOM Manipulation - Insert templates into the page
- Best Practices - Security and performance tips
DOM Manipulation Basics
Master the Document Object Model and dynamic page updates
Selecting Elements
Modern methods like querySelector and querySelectorAll provide powerful, flexible element selection using CSS selectors.
// 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.
// 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.
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.
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.
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
- Dynamic Table: Create a table component that renders data from an array and supports sorting, filtering
- Modal Dialog: Build a reusable modal with open/close animations and escape key handling
- Infinite Scroll: Implement infinite scrolling that loads more items as user scrolls to bottom
- Form Validator: Create real-time form validation with custom error messages and styling
- Drag and Drop: Build a drag-and-drop interface for reordering list items
- 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 - Deep dive into event handling
- Fetch API - Load data dynamically
- JSON & Storage - Persist data locally
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
- Execution Order Quiz: Write code with mixed sync/async/promises/timeouts and predict output before running
- Non-Blocking Sort: Implement a sorting algorithm that yields to the event loop every 1000 iterations
- Task Scheduler: Build a scheduler that runs tasks with priority (microtask > macrotask)
- Event Loop Monitor: Create a tool that detects and logs operations blocking the event loop > 50ms
- Web Worker Calculator: Build a calculator that offloads factorial computation to a Web Worker
- 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:
- Promises & Async/Await - Master asynchronous programming
- Timers & Async Patterns - Debounce, throttle, and more
- Error Handling - Debug async code effectively
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
- Debounced Search: Create a search input that debounces API calls by 500ms and shows loading state
- Throttled Scroll Progress: Build a reading progress bar that updates on scroll (throttled to 100ms)
- Animated Counter: Implement a counter that animates from 0 to a target number using requestAnimationFrame
- Task Queue: Build a task queue that limits concurrent API requests to 3 at a time
- Auto-Save Feature: Create an auto-save system that saves changes 2 seconds after user stops typing
- 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:
- Events & Event Loop - Understand JavaScript's execution model
- Promises & Async/Await - Master asynchronous programming
- Fetch API - Make HTTP requests effectively
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
- Wrap
setTimeout in a promise and await it to simulate delays.
- Chain two asynchronous operations and handle errors with a single
.catch.
- Use
Promise.all to fetch three resources simultaneously and handle a rejection.
- Rewrite a callback-based function to return a promise.
- Create an async function that runs two promises in parallel and another sequentially; log timing differences.
- Implement a retry helper that stops after n failed attempts.
- Compare
Promise.race and Promise.allSettled on mixed success/failure promises.
- Use
finally to clean up UI state after a promise chain.
- Demonstrate microtask ordering by mixing
Promise.resolve with setTimeout.
- 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
- User API Client: Build a complete API client class with GET, POST, PUT, DELETE methods, error handling, and authentication
- Search with Debounce: Implement a search feature that fetches results as user types, with debouncing and request cancellation
- File Upload with Progress: Create a file upload component using FormData with upload progress indication
- Retry Logic: Implement a fetch wrapper with exponential backoff retry logic and configurable retry count
- Request Queue: Build a request queue that limits concurrent requests to 3 at a time
- 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:
- Timers & Async Patterns - Master timing and async patterns
- Promises & Async/Await - Deep dive into asynchronous JavaScript
- Error Handling - Robust error management
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
- Error Handler: Build a global error handler that catches and logs all unhandled errors
- Custom Logger: Create a logger class with different log levels (DEBUG, INFO, WARN, ERROR)
- Error Boundary: Implement an error boundary component that catches errors in child components
- Debug Helper: Build a debug toolbar that shows performance metrics and logs
- Stack Trace Parser: Parse stack traces and format them for better readability
- 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:
- Testing & Linting - Prevent bugs before they happen
- Async/Await - Handle async errors
- Best Practices - Write maintainable, bug-free code
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
- Module Library: Create a utility library with multiple modules and export/import patterns
- Dynamic Router: Build a router that lazy-loads route components using dynamic imports
- npm Package: Create and publish a simple npm package with proper package.json configuration
- Build Pipeline: Set up a build process with webpack or vite including CSS and asset handling
- Monorepo Structure: Organize a project with multiple packages using workspaces
- 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:
- Testing & Linting - Quality tools and workflows
- Best Practices - Production-ready patterns
- Async/Await - Handle async module loading
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
- Create a class with a constructor and an instance method; instantiate it twice and compare method references.
- Implement getters and setters for a computed property like full name.
- Add static methods to generate IDs or cache instances.
- Build a base class and extend it with
super calls; override a method while reusing parent logic.
- Use private fields to protect internal counters; expose read-only getters.
- Demonstrate method binding to preserve
this when passing callbacks to setTimeout.
- Refactor an inheritance hierarchy into composition using factory functions.
- Create a mixin utility that copies methods onto a class prototype.
- Override
toString() on a class to customize logging.
- 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
- Create an object with
Object.create and add shared methods to its prototype object.
- Write a constructor function and attach methods to its prototype; verify instances share them.
- Implement inheritance between two constructors using
Object.create and reset constructor.
- Demonstrate the difference between own and inherited properties using
in and hasOwnProperty.
- Refactor duplicated methods defined in a constructor into prototype methods.
- Use
Object.getPrototypeOf to inspect an object's chain and log each step until null.
- Change an object's prototype at runtime with
Object.setPrototypeOf and observe new behavior.
- Compare memory usage by creating many instances with prototype methods versus inline methods (use console timing).
- Extend a base prototype with a new method and verify existing instances gain access.
- 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
- Calendar Generator: Build a function that generates a calendar for any month/year
- Date Picker Validator: Validate date inputs with min/max dates and disabled dates
- Countdown Timer: Create a countdown showing days, hours, minutes, seconds to a future date
- Time Tracker: Build a time tracking app that calculates duration between timestamps
- Business Days Calculator: Calculate the number of business days between two dates
- 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:
- Timers & Async Patterns - Work with timing functions
- JSON & Storage - Store dates in localStorage
- Modules & Tooling - Import date libraries
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
- Settings Manager: Build a settings component that persists user preferences with localStorage
- Shopping Cart: Create a shopping cart that persists items across sessions
- Form Auto-Save: Implement form data preservation with sessionStorage during page navigation
- Multi-Tab Sync: Build a notification system that syncs across browser tabs using storage events
- Offline Cache: Create a cache manager that stores API responses with TTL in localStorage
- 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:
- Fetch API - Combine with storage for caching strategies
- Modules & Tooling - Build production apps
- Best Practices - Security and performance tips
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