Modern JavaScript (ES2024): Your Practical Guide
The Goal: Master the JavaScript you'll use every day. Focus on modern patterns, async programming, and the features that make JavaScript powerful in 2024.
Table of Contents
- Introduction & Philosophy
- Core Concepts You Must Understand
- Essential Configuration & Setup
- Daily-Use Patterns
- Essential ES2024 Features
- Working with the Ecosystem
- Best Practices & Patterns
- Common Mistakes to Avoid
- Quick Reference
- Action Plan
1. Introduction & Philosophy
What is Modern JavaScript?
Modern JavaScript (ES6/ES2015+) is the evolution of JavaScript that introduced:
- Classes and modules for better code organization
- Promises and async/await for handling asynchronous operations
- Destructuring and spread for cleaner data manipulation
- Arrow functions for concise syntax
- Template literals for better string handling
ES2024 adds: Immutable array methods, better grouping, improved Set operations, and enhanced Unicode handling.
Why It Matters
Modern JavaScript lets you:
- Write cleaner, more maintainable code
- Handle async operations elegantly
- Work with data structures efficiently
- Avoid common pitfalls and bugs
- Use functional programming patterns
Mental Model
Think of JavaScript as having three layers:
- Synchronous code - Runs line by line
- Asynchronous code - Runs when operations complete (Promises, async/await)
- Event-driven code - Responds to user actions or system events
2. Core Concepts You Must Understand
2.1 Variables: const, let, and Block Scope
Use const by default, let when reassignment is needed
// Old way - avoid var
var name = "Alice";
var name = "Bob"; // Can accidentally redeclare
// Modern way
const PI = 3.14159; // Cannot be reassigned
// PI = 3.14; // Error
let count = 0; // Can be reassigned
count = 1; // OK
// Block scope - confined to { }
{
const temp = "local";
console.log(temp); // OK
}
// console.log(temp); // Error: temp not defined
Why This Matters:
constprevents accidental reassignment- Block scope prevents variable leakage
- Makes code more predictable and easier to debug
2.2 Arrow Functions vs Regular Functions
// Regular function
function add(a, b) {
return a + b;
}
// Arrow function (concise)
const add = (a, b) => a + b;
// Arrow function (with body)
const addAndLog = (a, b) => {
const result = a + b;
console.log(result);
return result;
};
// Single parameter - parentheses optional
const square = x => x * x;
// No parameters - parentheses required
const greet = () => console.log("Hello!");
Key Difference: this Binding
// Regular function - 'this' is dynamic
const person = {
name: "Alice",
greet: function() {
console.log(`Hello, ${this.name}`);
}
};
person.greet(); // "Hello, Alice"
// Arrow function - 'this' is lexical (inherited)
const person2 = {
name: "Bob",
greet: () => {
console.log(`Hello, ${this.name}`); // 'this' from outer scope
}
};
// Use case: Callbacks
class Timer {
constructor() {
this.seconds = 0;
// Arrow function preserves 'this'
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
When to Use Each:
- Arrow functions: Callbacks, array methods, when you want lexical
this - Regular functions: Methods in objects, constructors, when you need dynamic
this
2.3 Destructuring: Extract Values Cleanly
Array Destructuring
// Basic
const colors = ["red", "green", "blue"];
const [first, second, third] = colors;
console.log(first); // "red"
// Skip elements
const [primary, , tertiary] = colors;
console.log(tertiary); // "blue"
// Rest operator
const [head, ...tail] = colors;
console.log(tail); // ["green", "blue"]
// Default values
const [a, b, c = "default"] = ["x", "y"];
console.log(c); // "default"
// Swapping variables
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2, 1
Object Destructuring
// Basic
const user = {
id: 1,
name: "Alice",
email: "alice@example.com",
age: 30
};
const { name, email } = user;
console.log(name); // "Alice"
// Rename variables
const { name: userName, email: userEmail } = user;
console.log(userName); // "Alice"
// Default values
const { age, role = "user" } = user;
console.log(role); // "user"
// Nested destructuring
const person = {
name: "Bob",
address: {
city: "New York",
zip: "10001"
}
};
const { address: { city, zip } } = person;
console.log(city); // "New York"
// Function parameters (very common!)
function createUser({ name, email, age = 18 }) {
return {
id: Date.now(),
name,
email,
age,
createdAt: new Date()
};
}
createUser({ name: "Charlie", email: "charlie@example.com" });
Real-World Use Case: API Responses
// Instead of this
fetch("/api/user")
.then(response => response.json())
.then(data => {
console.log(data.user.name);
console.log(data.user.email);
});
// Do this
fetch("/api/user")
.then(response => response.json())
.then(({ user: { name, email } }) => {
console.log(name, email);
});
2.4 Template Literals: Better String Handling
// Old way
const name = "Alice";
const greeting = "Hello, " + name + "! You have " + 5 + " messages.";
// Modern way
const greeting = `Hello, ${name}! You have ${5} messages.`;
// Multi-line strings
const html = `
<div class="card">
<h2>${name}</h2>
<p>Welcome back!</p>
</div>
`;
// Expressions inside ${}
const price = 19.99;
const tax = 0.08;
const total = `Total: $${(price * (1 + tax)).toFixed(2)}`;
// Tagged templates (advanced)
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
return `${result}${str}<mark>${values[i] || ""}</mark>`;
}, "");
}
const user = "Alice";
const action = "logged in";
const message = highlight`User ${user} just ${action}`;
// "User <mark>Alice</mark> just <mark>logged in</mark>"
2.5 Spread and Rest Operators
Spread Operator (...): Expand iterables
// Array spreading
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Copy arrays (shallow)
const original = [1, 2, 3];
const copy = [...original];
// Object spreading
const defaults = { theme: "light", fontSize: 14 };
const userPrefs = { fontSize: 16, language: "en" };
const settings = { ...defaults, ...userPrefs };
// { theme: "light", fontSize: 16, language: "en" }
// Practical: Adding properties
const user = { name: "Alice", age: 30 };
const userWithId = { id: 1, ...user };
// Practical: Updating immutably
const state = { count: 5, items: [] };
const newState = { ...state, count: state.count + 1 };
Rest Operator (...): Collect remaining elements
// Function parameters
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5); // 15
// With other parameters
function greet(greeting, ...names) {
return `${greeting} ${names.join(", ")}!`;
}
greet("Hello", "Alice", "Bob", "Charlie");
// "Hello Alice, Bob, Charlie!"
// Array destructuring
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(rest); // [2, 3, 4, 5]
// Object destructuring
const { name, ...otherProps } = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
console.log(otherProps); // { age: 30, email: "alice@example.com" }
3. Essential Configuration & Setup
3.1 Package.json (Type: Module)
{
"name": "my-project",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js"
},
"engines": {
"node": ">=18.0.0"
}
}
Why "type": "module"?
- Enables ES6 import/export syntax
- Modern module system
- Better than CommonJS for new projects
3.2 ESLint Configuration (Code Quality)
// eslint.config.js
export default [
{
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
globals: {
console: "readonly",
process: "readonly"
}
},
rules: {
"no-var": "error", // Force const/let
"prefer-const": "error", // Use const when possible
"no-unused-vars": "warn", // Catch unused variables
"no-console": "off", // Allow console in development
"prefer-arrow-callback": "warn" // Prefer arrow functions
}
}
];
3.3 Modern Browser Support
Target Modern Browsers (2024):
- Chrome 120+
- Firefox 120+
- Safari 17+
- Edge 120+
- Node.js 18+
Transpiling for Older Browsers:
# Use Babel if needed
npm install --save-dev @babel/core @babel/preset-env
// babel.config.json
{
"presets": [
["@babel/preset-env", {
"targets": "> 0.25%, not dead"
}]
]
}
4. Daily-Use Patterns
4.1 Async/Await: Modern Async Programming
The Problem:
// Callback hell
getUserData(userId, function(error, user) {
if (error) {
console.error(error);
} else {
getPosts(user.id, function(error, posts) {
if (error) {
console.error(error);
} else {
getComments(posts[0].id, function(error, comments) {
console.log(comments);
});
}
});
}
});
Solution: Async/Await
async function loadUserData(userId) {
try {
const user = await getUserData(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return comments;
} catch (error) {
console.error("Error loading data:", error);
throw error;
}
}
// Usage
const comments = await loadUserData(123);
Practical Pattern: API Calls
// Fetch wrapper
async function fetchJSON(url, options = {}) {
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// Usage
async function getUser(id) {
try {
const user = await fetchJSON(`/api/users/${id}`);
return user;
} catch (error) {
console.error("Failed to fetch user:", error);
return null;
}
}
Parallel Execution
// Sequential (slow)
const user = await getUser(1);
const posts = await getPosts(1);
const comments = await getComments(1);
// Parallel (fast)
const [user, posts, comments] = await Promise.all([
getUser(1),
getPosts(1),
getComments(1)
]);
// With error handling per promise
const results = await Promise.allSettled([
getUser(1),
getPosts(1),
getComments(1)
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Result ${index}:`, result.value);
} else {
console.error(`Error ${index}:`, result.reason);
}
});
4.2 Array Methods: Functional Programming
Map: Transform each element
const prices = [19.99, 29.99, 39.99];
// Add tax to each price
const withTax = prices.map(price => price * 1.08);
console.log(withTax); // [21.59, 32.39, 43.19]
// Real-world: Transform API data
const users = await fetchJSON("/api/users");
const usernames = users.map(user => user.name);
// Extract specific properties
const products = [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Mouse", price: 29 }
];
const productInfo = products.map(({ id, name }) => ({ id, name }));
Filter: Select elements that match
const numbers = [1, 2, 3, 4, 5, 6];
// Get even numbers
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4, 6]
// Real-world: Filter active users
const activeUsers = users.filter(user => user.isActive);
// Multiple conditions
const premiumActive = users.filter(user =>
user.isActive && user.subscription === "premium"
);
// Filter out falsy values
const values = [0, 1, false, 2, "", 3, null, undefined];
const truthy = values.filter(Boolean);
console.log(truthy); // [1, 2, 3]
Reduce: Combine into single value
const numbers = [1, 2, 3, 4, 5];
// Sum
const sum = numbers.reduce((total, n) => total + n, 0);
console.log(sum); // 15
// Real-world: Calculate total price
const cart = [
{ name: "Laptop", price: 999, quantity: 1 },
{ name: "Mouse", price: 29, quantity: 2 }
];
const total = cart.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
console.log(total); // 1057
// Group by property
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "admin" }
];
const byRole = users.reduce((groups, user) => {
const role = user.role;
groups[role] = groups[role] || [];
groups[role].push(user);
return groups;
}, {});
// { admin: [Alice, Charlie], user: [Bob] }
Find: Get first matching element
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
];
// Find by ID
const user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: "Bob" }
// Returns undefined if not found
const notFound = users.find(u => u.id === 999);
console.log(notFound); // undefined
// FindIndex: Get position
const index = users.findIndex(u => u.id === 2);
console.log(index); // 1
Some & Every: Boolean checks
const ages = [18, 21, 25, 30];
// Some: At least one matches
const hasMinor = ages.some(age => age < 18);
console.log(hasMinor); // false
const hasAdult = ages.some(age => age >= 18);
console.log(hasAdult); // true
// Every: All must match
const allAdults = ages.every(age => age >= 18);
console.log(allAdults); // true
// Real-world: Form validation
const fields = [
{ name: "email", value: "alice@example.com", valid: true },
{ name: "password", value: "secret123", valid: true }
];
const isFormValid = fields.every(field => field.valid);
Chaining Methods
// Get total price of active products
const products = [
{ name: "Laptop", price: 999, active: true },
{ name: "Mouse", price: 29, active: true },
{ name: "Keyboard", price: 79, active: false }
];
const total = products
.filter(p => p.active)
.map(p => p.price)
.reduce((sum, price) => sum + price, 0);
console.log(total); // 1028
4.3 Optional Chaining & Nullish Coalescing
Optional Chaining (?.):
const user = {
name: "Alice",
address: {
city: "New York"
}
};
// Old way - verbose
const city = user && user.address && user.address.city;
// Optional chaining
const city = user?.address?.city;
console.log(city); // "New York"
// With missing data
const country = user?.address?.country;
console.log(country); // undefined (no error!)
// Array access
const firstPost = user?.posts?.[0];
// Method calls
const result = user?.getProfile?.();
// Real-world: API responses
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return {
name: data?.user?.name ?? "Unknown",
email: data?.user?.email ?? "N/A",
city: data?.user?.address?.city ?? "Not specified"
};
}
Nullish Coalescing (??):
// Returns right side only if left is null/undefined
// (not for 0, "", false)
const count = 0;
// Wrong - uses falsy check
const value1 = count || 10;
console.log(value1); // 10 (unwanted!)
// Correct - checks null/undefined only
const value2 = count ?? 10;
console.log(value2); // 0 (correct!)
// Real-world: Default values
function createUser(options = {}) {
return {
name: options.name ?? "Anonymous",
age: options.age ?? 18,
theme: options.theme ?? "light"
};
}
createUser({ age: 0 });
// { name: "Anonymous", age: 0, theme: "light" }
// Combined with optional chaining
const config = {
server: {
port: 3000
}
};
const port = config?.server?.port ?? 8080;
const host = config?.server?.host ?? "localhost";
4.4 Promises: The Foundation of Async
Creating Promises:
// Basic promise
const delay = (ms) => new Promise(resolve => {
setTimeout(resolve, ms);
});
await delay(1000);
console.log("1 second later");
// Promise with value
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Alice" });
} else {
reject(new Error("Invalid ID"));
}
}, 1000);
});
}
// Using the promise
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error(error));
Promise Combinators:
// Promise.all - Wait for all (fails if any fails)
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json())
]);
// Promise.allSettled - Wait for all (never fails)
const results = await Promise.allSettled([
fetch("/api/users"),
fetch("/api/posts"),
fetch("/api/comments")
]);
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason);
}
});
// Promise.race - First to complete wins
const fastest = await Promise.race([
fetch("/api/server1/data"),
fetch("/api/server2/data"),
fetch("/api/server3/data")
]);
// Promise.any - First to succeed wins (ignores rejections)
const firstSuccess = await Promise.any([
fetch("/api/backup1/data"),
fetch("/api/backup2/data"),
fetch("/api/backup3/data")
]);
Real-World Pattern: Timeout
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Timeout")), ms);
});
return Promise.race([promise, timeout]);
}
// Usage
try {
const data = await withTimeout(
fetch("/api/slow-endpoint"),
5000
);
} catch (error) {
console.error("Request timed out or failed:", error);
}
4.5 Modules: Import/Export
Named Exports:
// utils.js
export const API_URL = "https://api.example.com";
export function formatDate(date) {
return date.toLocaleDateString();
}
export function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Alternative syntax
const helper1 = () => {};
const helper2 = () => {};
export { helper1, helper2 };
Named Imports:
// main.js
import { API_URL, formatDate, calculateTotal } from "./utils.js";
console.log(API_URL);
const formatted = formatDate(new Date());
const total = calculateTotal(items);
// Import with rename
import { formatDate as format } from "./utils.js";
// Import everything
import * as utils from "./utils.js";
utils.formatDate(new Date());
Default Export:
// User.js
export default class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
// Or function
export default function createUser(name) {
return { name, createdAt: new Date() };
}
Default Import:
// main.js
import User from "./User.js";
import createUser from "./createUser.js";
const user = new User("Alice");
const newUser = createUser("Bob");
Mixing Named and Default:
// api.js
export default class API {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
}
export const API_VERSION = "v1";
export const TIMEOUT = 5000;
// main.js
import API, { API_VERSION, TIMEOUT } from "./api.js";
Dynamic Imports:
// Load module conditionally
if (condition) {
const module = await import("./heavyModule.js");
module.doSomething();
}
// Lazy loading
button.addEventListener("click", async () => {
const { animate } = await import("./animations.js");
animate(element);
});
5. Essential ES2024 Features
5.1 Immutable Array Methods
Problem with Mutating Methods:
const numbers = [3, 1, 4, 1, 5, 9];
numbers.sort();
console.log(numbers); // [1, 1, 3, 4, 5, 9] - ORIGINAL CHANGED!
// This breaks in React/Redux when you need immutability
ES2024 Solution: Non-Mutating Methods
const numbers = [3, 1, 4, 1, 5, 9];
// toSorted() - Returns new sorted array
const sorted = numbers.toSorted();
console.log(sorted); // [1, 1, 3, 4, 5, 9]
console.log(numbers); // [3, 1, 4, 1, 5, 9] - UNCHANGED!
// toReversed() - Returns new reversed array
const reversed = numbers.toReversed();
console.log(reversed); // [9, 5, 1, 4, 1, 3]
// toSpliced() - Returns new array with elements removed/added
const spliced = numbers.toSpliced(2, 1, 99, 100);
console.log(spliced); // [3, 1, 99, 100, 1, 5, 9]
// with() - Returns new array with element replaced
const replaced = numbers.with(0, 999);
console.log(replaced); // [999, 1, 4, 1, 5, 9]
Real-World Use Case: React State
// Old way - easy to mutate state accidentally
const [items, setItems] = useState([1, 2, 3]);
function sortItems() {
const sorted = items.sort(); // MUTATES ORIGINAL!
setItems([...sorted]); // Workaround needed
}
// ES2024 way - safe by default
function sortItems() {
setItems(items.toSorted()); // Returns new array automatically
}
function removeItem(index) {
setItems(items.toSpliced(index, 1));
}
function updateItem(index, value) {
setItems(items.with(index, value));
}
5.2 Object.groupBy() and Map.groupBy()
Problem: Grouping requires verbose reduce:
// Old way
const products = [
{ name: "Laptop", category: "electronics", price: 999 },
{ name: "Mouse", category: "electronics", price: 29 },
{ name: "Desk", category: "furniture", price: 299 }
];
const grouped = products.reduce((groups, product) => {
const category = product.category;
groups[category] = groups[category] || [];
groups[category].push(product);
return groups;
}, {});
ES2024 Solution: Object.groupBy()
const grouped = Object.groupBy(products, product => product.category);
console.log(grouped);
// {
// electronics: [
// { name: "Laptop", category: "electronics", price: 999 },
// { name: "Mouse", category: "electronics", price: 29 }
// ],
// furniture: [
// { name: "Desk", category: "furniture", price: 299 }
// ]
// }
// Group by price range
const byPriceRange = Object.groupBy(products, product => {
if (product.price < 50) return "budget";
if (product.price < 500) return "mid-range";
return "premium";
});
// Group by boolean condition
const byAvailability = Object.groupBy(products, product =>
product.inStock ? "available" : "out-of-stock"
);
Map.groupBy() for Non-String Keys:
const users = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 25 }
];
// Use Map.groupBy for number/object keys
const byAge = Map.groupBy(users, user => user.age);
console.log(byAge.get(25));
// [{ name: "Alice", age: 25 }, { name: "Charlie", age: 25 }]
console.log(byAge.get(30));
// [{ name: "Bob", age: 30 }]
Real-World Pattern: Group and Transform
// Group orders by status and calculate totals
const orders = [
{ id: 1, status: "completed", total: 100 },
{ id: 2, status: "completed", total: 150 },
{ id: 3, status: "pending", total: 80 }
];
const ordersByStatus = Object.groupBy(orders, o => o.status);
const summary = Object.entries(ordersByStatus).map(([status, orders]) => ({
status,
count: orders.length,
total: orders.reduce((sum, o) => sum + o.total, 0)
}));
console.log(summary);
// [
// { status: "completed", count: 2, total: 250 },
// { status: "pending", count: 1, total: 80 }
// ]
5.3 Promise.withResolvers()
Problem: Need to access resolve/reject outside Promise:
// Old way - verbose
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// Later...
someAsyncOperation().then(resolve).catch(reject);
ES2024 Solution:
const { promise, resolve, reject } = Promise.withResolvers();
// Use resolve/reject whenever needed
setTimeout(() => resolve("Done!"), 2000);
const result = await promise;
console.log(result); // "Done!" (after 2 seconds)
Real-World Use Case: Event-Based Async
// Wait for user interaction
function waitForClick(button) {
const { promise, resolve } = Promise.withResolvers();
button.addEventListener("click", () => resolve(), { once: true });
return promise;
}
// Usage
await waitForClick(document.querySelector("#submit"));
console.log("Button clicked!");
// WebSocket wrapper
class WebSocketClient {
constructor(url) {
const { promise, resolve, reject } = Promise.withResolvers();
this.ws = new WebSocket(url);
this.connected = promise;
this.ws.onopen = () => resolve();
this.ws.onerror = (error) => reject(error);
}
async send(message) {
await this.connected;
this.ws.send(message);
}
}
const client = new WebSocketClient("ws://localhost:3000");
await client.send("Hello!"); // Waits for connection automatically
5.4 Set Methods: Union, Intersection, Difference
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// Union - Elements in either set
const union = setA.union(setB);
console.log([...union]); // [1, 2, 3, 4, 5, 6]
// Intersection - Elements in both sets
const intersection = setA.intersection(setB);
console.log([...intersection]); // [3, 4]
// Difference - Elements in A but not in B
const difference = setA.difference(setB);
console.log([...difference]); // [1, 2]
// Symmetric Difference - Elements in either but not both
const symDiff = setA.symmetricDifference(setB);
console.log([...symDiff]); // [1, 2, 5, 6]
// isSubsetOf, isSupersetOf, isDisjointFrom
const small = new Set([1, 2]);
const large = new Set([1, 2, 3, 4]);
console.log(small.isSubsetOf(large)); // true
console.log(large.isSupersetOf(small)); // true
console.log(small.isDisjointFrom(setB)); // true (no common elements)
Real-World Use Cases:
// User permissions
const adminPerms = new Set(["read", "write", "delete", "admin"]);
const userPerms = new Set(["read", "write"]);
const hasAllPerms = userPerms.isSubsetOf(adminPerms);
const extraPerms = adminPerms.difference(userPerms);
console.log([...extraPerms]); // ["delete", "admin"]
// Feature flags
const enabledFeatures = new Set(["darkMode", "newUI", "beta"]);
const requestedFeatures = new Set(["darkMode", "analytics"]);
const available = enabledFeatures.intersection(requestedFeatures);
const unavailable = requestedFeatures.difference(enabledFeatures);
// Deduplication across arrays
const array1 = [1, 2, 3, 4];
const array2 = [3, 4, 5, 6];
const combined = new Set(array1).union(new Set(array2));
const unique = [...combined]; // [1, 2, 3, 4, 5, 6]
5.5 Well-Formed Unicode Strings
// Check if string has valid Unicode
const valid = "Hello 世界 ";
const malformed = "Hello\uD800World"; // Lone surrogate
console.log(valid.isWellFormed()); // true
console.log(malformed.isWellFormed()); // false
// Fix malformed strings
const fixed = malformed.toWellFormed();
console.log(fixed); // "Hello�World" (� is replacement character)
// Real-world: Safe API processing
function processUserInput(text) {
if (!text.isWellFormed()) {
console.warn("Malformed Unicode detected, fixing...");
text = text.toWellFormed();
}
return text.normalize("NFC"); // Further normalization
}
// Safe encoding for transmission
function safeEncode(str) {
const wellFormed = str.toWellFormed();
return new TextEncoder().encode(wellFormed);
}
// Validate before database storage
function saveComment(comment) {
if (!comment.isWellFormed()) {
throw new Error("Invalid Unicode in comment");
}
// Save to database
db.insert({ comment });
}
6. Working with the Ecosystem
6.1 Fetch API: Modern HTTP Requests
// GET request
async function getUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// POST request
async function createUser(userData) {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(userData)
});
return response.json();
}
// With authentication
async function fetchWithAuth(url, token) {
const response = await fetch(url, {
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
}
});
return response.json();
}
// Upload file
async function uploadFile(file) {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData // Don't set Content-Type, browser sets it
});
return response.json();
}
// Reusable fetch wrapper
async function api(endpoint, options = {}) {
const response = await fetch(`https://api.example.com${endpoint}`, {
headers: {
"Content-Type": "application/json",
...options.headers
},
...options
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
// Usage
const users = await api("/users");
const newUser = await api("/users", {
method: "POST",
body: JSON.stringify({ name: "Alice" })
});
6.2 LocalStorage & SessionStorage
// Save data
localStorage.setItem("user", JSON.stringify({ name: "Alice", id: 1 }));
sessionStorage.setItem("token", "abc123");
// Retrieve data
const user = JSON.parse(localStorage.getItem("user"));
const token = sessionStorage.getItem("token");
// Remove data
localStorage.removeItem("user");
sessionStorage.clear(); // Clear all
// Helper functions
const storage = {
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
get(key) {
const item = localStorage.getItem(key);
try {
return JSON.parse(item);
} catch {
return item;
}
},
remove(key) {
localStorage.removeItem(key);
}
};
// Usage
storage.set("user", { name: "Alice", age: 30 });
const user = storage.get("user");
6.3 URL and URLSearchParams
// Parse URL
const url = new URL("https://api.example.com/users?page=2&limit=10");
console.log(url.protocol); // "https:"
console.log(url.hostname); // "api.example.com"
console.log(url.pathname); // "/users"
console.log(url.search); // "?page=2&limit=10"
// Work with query parameters
const params = url.searchParams;
console.log(params.get("page")); // "2"
console.log(params.get("limit")); // "10"
params.set("page", "3");
params.append("sort", "name");
params.delete("limit");
console.log(url.toString());
// "https://api.example.com/users?page=3&sort=name"
// Build query string
const searchParams = new URLSearchParams({
page: 1,
limit: 20,
sort: "date",
filter: ["active", "verified"]
});
const apiUrl = `https://api.example.com/users?${searchParams}`;
// Helper function
function buildUrl(base, params) {
const url = new URL(base);
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => url.searchParams.append(key, v));
} else if (value != null) {
url.searchParams.set(key, value);
}
});
return url.toString();
}
const url = buildUrl("https://api.example.com/search", {
q: "javascript",
category: ["tutorial", "reference"],
page: 1
});
7. Best Practices & Patterns
DO: Use const by default
// Prevents accidental reassignment
const API_URL = "https://api.example.com";
const users = [];
// Use let only when needed
for (let i = 0; i < 10; i++) {
console.log(i);
}
DO: Use async/await over .then()
// Promise chains get messy
fetch("/api/user")
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(response => response.json())
.then(posts => console.log(posts));
// Async/await is cleaner
async function getUserPosts() {
const userResponse = await fetch("/api/user");
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
return posts;
}
DO: Use destructuring in function parameters
// Accessing properties repeatedly
function createUser(options) {
return {
name: options.name,
email: options.email,
age: options.age || 18
};
}
// Destructure parameters
function createUser({ name, email, age = 18 }) {
return { name, email, age };
}
DO: Use array methods over loops
// Manual loop
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
// Array method
const doubled = numbers.map(n => n * 2);
DO: Handle errors properly
// Always use try-catch with async/await
async function fetchData() {
try {
const response = await fetch("/api/data");
return await response.json();
} catch (error) {
console.error("Failed to fetch:", error);
return null; // Or throw, depending on needs
}
}
DO: Use optional chaining and nullish coalescing
// Verbose null checks
const city = user && user.address && user.address.city
? user.address.city
: "Unknown";
// Concise modern syntax
const city = user?.address?.city ?? "Unknown";
DO: Keep functions small and focused
// Single responsibility
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async function validateUser(user) {
return user.email && user.name;
}
async function saveUser(user) {
return fetch("/api/users", {
method: "POST",
body: JSON.stringify(user)
});
}
8. Common Mistakes to Avoid
Mistake 1: Forgetting await
// Returns Promise, not value
async function getUser() {
return fetch("/api/user").then(r => r.json());
}
const user = getUser(); // Promise, not user data!
console.log(user.name); // undefined or error
// Always await async functions
const user = await getUser();
console.log(user.name); // Works!
Mistake 2: Mutating arrays/objects directly
// Mutates original
const numbers = [1, 2, 3];
numbers.sort(); // Original changed!
// Use immutable methods (ES2024)
const sorted = numbers.toSorted();
// Or spread for older approaches
const sorted = [...numbers].sort();
Mistake 3: Not handling async errors
// Unhandled rejection
async function fetchData() {
const data = await fetch("/api/data"); // Might fail!
return data.json();
}
// Always use try-catch
async function fetchData() {
try {
const response = await fetch("/api/data");
return await response.json();
} catch (error) {
console.error(error);
return null;
}
}
Mistake 4: Sequential instead of parallel
// Slow - waits for each to finish
const user = await getUser(1);
const posts = await getPosts(1);
const comments = await getComments(1);
// Fast - runs in parallel
const [user, posts, comments] = await Promise.all([
getUser(1),
getPosts(1),
getComments(1)
]);
Mistake 5: Forgetting return in array methods
// Returns undefined
const doubled = numbers.map(n => {
n * 2; // Missing return!
});
// Implicit return with arrow functions
const doubled = numbers.map(n => n * 2);
// Or explicit return
const doubled = numbers.map(n => {
return n * 2;
});
Mistake 6: Using == instead of ===
// Type coercion leads to bugs
0 == false; // true (wat?)
"" == false; // true
null == undefined; // true
// Always use strict equality
0 === false; // false
"" === false; // false
null === undefined; // false
Mistake 7: Not closing over variables properly
// Common mistake in loops
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (not 0, 1, 2!)
// Use let (block scope)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2
9. Quick Reference
Variables
const name = "Alice"; // Immutable binding
let count = 0; // Mutable binding
Functions
// Arrow function
const add = (a, b) => a + b;
// Async function
async function fetchData() {
const data = await fetch(url);
return data.json();
}
Destructuring
const { name, age } = user;
const [first, ...rest] = array;
Array Methods
array.map(x => x * 2)
array.filter(x => x > 0)
array.reduce((sum, x) => sum + x, 0)
array.find(x => x.id === 1)
array.some(x => x > 10)
array.every(x => x > 0)
ES2024
array.toSorted()
array.toReversed()
array.with(0, value)
Object.groupBy(array, x => x.category)
Promise.withResolvers()
setA.union(setB)
str.isWellFormed()
Async Patterns
await promise
await Promise.all([p1, p2])
await Promise.race([p1, p2])
Optional & Nullish
obj?.property?.nested
value ?? defaultValue
10. Action Plan
Week 1: Foundations
Master const/let, arrow functions, template literals
Practice destructuring in different scenarios
Use spread/rest operators daily
Convert existing code to use these features
Week 2: Async Mastery
Replace callbacks with async/await Practice with Promise.all, Promise.race Build a simple API wrapper Handle errors properly in async code
Week 3: Array Methods
Replace all for loops with map/filter/reduce
Practice chaining array methods
Use ES2024 immutable methods
Group data with Object.groupBy()
Week 4: Real Projects
Build a REST API client with proper error handling Create a data transformation pipeline Implement async workflows with multiple API calls Use modules to organize code
Continuing Education
Follow TC39 proposals: https://github.com/tc39/proposals Read JavaScript.info: https://javascript.info/ Practice on LeetCode with modern JS Join JavaScript Weekly newsletter
Resources
Official Documentation
- MDN Web Docs - Most comprehensive
- TC39 ECMAScript Proposals - Upcoming features
- Can I Use - Browser compatibility
Learning Platforms
- JavaScript.info - In-depth tutorials
- FreeCodeCamp - Interactive learning
- Eloquent JavaScript - Free book
Stay Updated
- JavaScript Weekly - Newsletter
- 2ality Blog - In-depth articles by Dr. Axel Rauschmayer
- ES Discuss - Language development discussions
Remember
Modern JavaScript is about:
- Writing cleaner, more readable code
- Handling async operations elegantly
- Using immutability for predictable state
- Leveraging built-in methods over manual loops
- Organizing code with modules
Start small, practice daily, and gradually adopt these patterns. You don't need to use every feature immediately—focus on the ones that solve your current problems!
Happy coding!