There are exactly 8 falsy values: false, 0, -0, 0n (BigInt zero), "" (empty string), null, undefined, and NaN. Everything else — including empty arrays, empty objects, and the string "false" — is truthy. Knowing this list prevents subtle bugs in conditionals and short-circuit expressions.
// All falsy
Boolean(false) // false
Boolean(0) // false
Boolean("") // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
// Surprises — these are TRUTHY
Boolean([]) // true ← empty array!
Boolean({}) // true ← empty object!
Boolean("false") // true ← non-empty string!
Boolean("0") // true ← non-empty string!
// Practical: checking for a real value
const name = "";
if (name) { /* skipped — empty string is falsy */ }
// Use explicit checks when 0 is a valid value
const count = 0;
if (count !== undefined) { /* runs correctly ✓ */ }
parseInt() reads characters from left to right, stops at the first non-numeric character, and always returns an integer. parseFloat() does the same but preserves decimals. Number() converts the entire value strictly — any non-numeric character in a string returns NaN. Always pass a radix to parseInt() to avoid octal surprises.
parseInt("42px", 10) // 42 — stops at "p"
parseInt("3.9", 10) // 3 — truncates decimal
parseFloat("3.9") // 3.9
parseFloat("3.9em") // 3.9 — stops at "e"
Number("42") // 42
Number("42px") // NaN — strict, entire string must be numeric
Number("") // 0 ← surprising!
Number(null) // 0 ← surprising!
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
// Safe check
Number.isFinite(Number("42px")) // false
Number.isNaN(NaN) // true (better than global isNaN)
Strings are immutable — every method returns a new string. The most-used methods cover searching (includes, indexOf, startsWith, endsWith), extraction (slice), transformation (replace, toUpperCase, trim), and splitting (split). Knowing which returns what prevents hours of debugging.
const s = " Hello, World! ";
s.trim() // "Hello, World!"
s.toLowerCase() // " hello, world! "
s.includes("World") // true
s.startsWith(" Hello") // true
s.indexOf("o") // 4 (first occurrence)
s.lastIndexOf("o") // 9
s.slice(2, 7) // "Hello"
s.slice(-8, -2) // "World!" — negative indices
s.replace("World", "JS") // " Hello, JS! "
s.replaceAll("l", "L") // " HeLLo, WorLd! "
s.split(", ") // [" Hello", "World! "]
"ha".repeat(3) // "hahaha"
s.trimEnd().endsWith("!") // true
slice() is non-destructive — it returns a new array from a range of indices without touching the original. splice() is destructive — it modifies the original array in place, removing, replacing, or inserting elements at a given position and returning the removed items.
const fruits = ["apple", "banana", "cherry", "date", "elderberry"];
// slice — original untouched ✓
const middle = fruits.slice(1, 4); // ["banana", "cherry", "date"]
console.log(fruits.length); // 5 — unchanged
// splice(start, deleteCount, ...itemsToInsert)
const removed = fruits.splice(1, 2, "blueberry");
// removed = ["banana", "cherry"]
// fruits = ["apple", "blueberry", "date", "elderberry"]
// splice to insert without removing
fruits.splice(2, 0, "coconut");
// fruits = ["apple", "blueberry", "coconut", "date", "elderberry"]
Math is a built-in object (not a constructor) with static methods for common mathematical operations. The most frequently needed are round, floor, ceil, abs, min, max, pow, sqrt, and random. Understanding Math.random()‘s range and how to scale it is essential for games, simulations, and shuffle algorithms.
Math.round(4.5) // 5 — rounds to nearest integer
Math.floor(4.9) // 4 — always rounds down
Math.ceil(4.1) // 5 — always rounds up
Math.abs(-7) // 7
Math.max(3, 9, 1) // 9
Math.min(3, 9, 1) // 1
Math.pow(2, 10) // 1024 (same as 2 ** 10)
Math.sqrt(16) // 4
Math.trunc(-4.9) // -4 — removes decimal, toward zero
// Math.random() returns [0, 1)
Math.random() // e.g. 0.7341...
// Random integer between min (inclusive) and max (exclusive)
const randInt = (min, max) => Math.floor(Math.random() * (max - min)) + min;
randInt(1, 7); // simulates a die roll: 1–6
A Map preserves insertion order, accepts any value (including objects and functions) as a key, and exposes a reliable .size property. A plain object coerces all keys to strings, inherits prototype properties that can cause key collisions, and requires Object.keys() to count entries. For frequent add/delete operations, Map is generally faster.
const map = new Map();
const keyObj = { id: 1 };
map.set(keyObj, "object as key"); // ✓ any type as key
map.set(42, "number key");
map.set(true, "boolean key");
console.log(map.size); // 3
console.log(map.get(keyObj)); // "object as key"
// Plain object — keys always become strings
const obj = {};
obj[keyObj] = "value";
console.log(Object.keys(obj)); // ["[object Object]"] ⚠️
A Set stores only unique values — adding a duplicate is silently ignored. Arrays allow duplicates and support index-based access, while Set has no indexes but offers O(1) lookups via .has(). Sets are ideal for deduplication and membership testing.
const set = new Set([1, 2, 2, 3, 3, 3]);
console.log([...set]); // [1, 2, 3] — duplicates removed
// Fast membership check
set.has(2); // true (O(1))
// Deduplicate an array
const tags = ["js", "css", "js", "html", "css"];
const unique = [...new Set(tags)]; // ["js", "css", "html"]
// Set operations (ES2024 native)
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.intersection(b); // Set {2, 3}
A WeakMap holds weak references to its object keys — if no other reference to a key exists, the garbage collector can reclaim both the key and its value automatically. It is not iterable and has no .size. Use it to attach private metadata to objects without preventing garbage collection.
const cache = new WeakMap();
function processUser(user) {
if (cache.has(user)) return cache.get(user);
const result = expensiveComputation(user);
cache.set(user, result); // key is the user object
return result;
}
// When user goes out of scope → entry is GC'd automatically
// No manual cleanup needed ✓
for...in iterates over an object’s enumerable property keys (including inherited ones), making it useful for plain objects but dangerous on arrays. for...of iterates over the values of any iterable (arrays, strings, Maps, Sets) and ignores prototype properties entirely.
const arr = [10, 20, 30];
for (const key of arr) console.log(key); // 10 20 30 ✓
for (const key in arr) console.log(key); // "0" "1" "2" (indices, strings!)
const user = { name: "Ana", age: 28 };
for (const key in user) console.log(key, user[key]);
// name Ana
// age 28
// for...of on a Map gives [key, value] pairs
const map = new Map([["a", 1], ["b", 2]]);
for (const [k, v] of map) console.log(k, v);
Optional chaining short-circuits and returns undefined the moment it encounters a null or undefined in a property access chain, instead of throwing a TypeError. It works on property access, method calls, and bracket notation.
const user = { profile: { address: { city: "Tel Aviv" } } };
// Old way — verbose guard
const city = user && user.profile && user.profile.address
&& user.profile.address.city;
// Optional chaining
const city = user?.profile?.address?.city; // "Tel Aviv"
const zip = user?.profile?.address?.zip; // undefined (no throw)
// Works on methods and arrays too
user?.getPermissions?.();
user?.roles?.[0];
?? returns the right-hand operand only when the left-hand side is null or undefined. Unlike ||, it does not treat falsy values such as 0, "", or false as fallback triggers — making it safer for legitimate zero or empty-string values.
// Problem with ||
const volume = 0;
console.log(volume || 50); // 50 ⚠️ — 0 is falsy, wrong default!
// ?? only falls back on null / undefined
console.log(volume ?? 50); // 0 ✓
// Common with optional chaining
const timeout = config?.retry?.timeout ?? 3000;
// Nullish assignment (??=)
user.name ??= "Guest"; // assigns only if user.name is null/undefined
Getters and setters let you define computed or validated properties that look like plain property access from the outside. A getter runs when the property is read; a setter runs when it is assigned. They are useful for lazy computation, input validation, and encapsulating private state.
class Temperature {
#celsius; // private field
constructor(c) { this.#celsius = c; }
get fahrenheit() { // computed on read
return this.#celsius * 9/5 + 32;
}
set fahrenheit(f) { // validated on write
if (f < -459.67) throw new RangeError("Below absolute zero");
this.#celsius = (f - 32) * 5/9;
}
}
const t = new Temperature(100);
console.log(t.fahrenheit); // 212
t.fahrenheit = 32;
console.log(t.fahrenheit); // 32
An iterator is any object with a .next() method that returns { value, done }. An object is iterable when it implements the Symbol.iterator method that returns an iterator. Arrays, strings, Maps, and Sets are all built-in iterables. Custom iterables work with for...of, spread, and destructuring.
// Custom range iterable
const range = {
from: 1, to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
};
console.log([...range]); // [1, 2, 3, 4, 5]
for (const n of range) console.log(n); // 1 2 3 4 5
A generator (declared with function*) is a function that can pause its execution at each yield statement and resume on the next .next() call. Each call returns { value, done }. Generators produce lazy sequences — values are computed only when requested, making them efficient for infinite streams.
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
fib.next().value; // 0
fib.next().value; // 1
fib.next().value; // 1
fib.next().value; // 2
fib.next().value; // 3
// Take first N values
function take(gen, n) {
return [...Array(n)].map(() => gen.next().value);
}
take(fibonacci(), 7); // [0, 1, 1, 2, 3, 5, 8]
A Symbol is a unique, immutable primitive value — no two symbols are ever equal, even if created with the same description. They are used as guaranteed-unique object property keys to avoid naming collisions, and as well-known hooks into the language runtime (e.g. Symbol.iterator, Symbol.toPrimitive).
const id = Symbol("id");
const user = { name: "Alice", [id]: 42 };
console.log(user[id]); // 42
console.log(Object.keys(user)); // ["name"] — symbol hidden ✓
// Every symbol is unique
Symbol("x") === Symbol("x"); // false
// Well-known symbol — custom toString coercion
class Money {
constructor(amount) { this.amount = amount; }
[Symbol.toPrimitive](hint) {
return hint === "string" ? `$${this.amount}` : this.amount;
}
}
const price = new Money(99);
console.log(`Total: ${price}`); // "Total: $99"
console.log(price + 1); // 100
Code in try runs normally. If any exception is thrown, execution jumps immediately to catch, which receives the error object. finally runs unconditionally — whether or not an error occurred — making it ideal for cleanup (closing files, releasing locks). A return inside finally overrides any earlier return value.
async function loadData(url) {
let response;
try {
response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
if (err instanceof TypeError) {
console.error("Network failure:", err.message);
} else {
console.error("Server error:", err.message);
}
return null;
} finally {
console.log("Request finished"); // always runs ✓
}
}
Extend the built-in Error class to create typed, domain-specific errors. Custom errors let callers use instanceof checks to handle different failure modes distinctly without relying on string parsing of the message field.
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NetworkError extends Error {
constructor(status, message) {
super(message);
this.name = "NetworkError";
this.status = status;
}
}
try {
throw new ValidationError("email", "Invalid format");
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Field "${err.field}": ${err.message}`);
} else if (err instanceof NetworkError) {
console.log(`HTTP ${err.status}: ${err.message}`);
}
}
In synchronous code, unhandled errors bubble up the call stack. In async code, a rejected Promise without a .catch() or try/catch becomes an unhandled rejection that can crash Node.js or fire a browser warning. Always handle errors at the boundary where you have enough context to act meaningfully.
// ❌ Unhandled rejection — dangerous
async function bad() { throw new Error("oops"); }
bad(); // UnhandledPromiseRejection ⚠️
// ✓ Handled at call site
bad().catch(err => console.error(err));
// ✓ Global safety net (Node.js)
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
process.exit(1);
});
// ✓ Global safety net (browser)
window.addEventListener("unhandledrejection", (e) => {
console.error("Unhandled rejection:", e.reason);
});
Event delegation attaches a single listener to a parent element instead of to each child. Because events bubble up the DOM tree, the parent catches events fired by any descendant. It is more efficient for large or dynamically generated lists, and the listener works for elements added after page load.
// ❌ One listener per item — expensive for 1000 rows
document.querySelectorAll(".item").forEach(el => {
el.addEventListener("click", handleClick);
});
// ✓ One listener on the parent
document.getElementById("list").addEventListener("click", (e) => {
const item = e.target.closest(".item");
if (!item) return; // click was outside an item
console.log("Clicked:", item.dataset.id);
});
// Works for dynamically added items too ✓
list.insertAdjacentHTML("beforeend",
'New ');
localStorage persists until explicitly cleared — data survives page reloads, tab closes, and browser restarts. sessionStorage is tied to the browser tab's lifetime — closing the tab wipes the data. Both are synchronous, string-only, and origin-scoped with a ~5 MB limit; neither is a substitute for secure server-side storage of sensitive data.
// localStorage — persists across sessions
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark"
localStorage.removeItem("theme");
// sessionStorage — cleared when tab closes
sessionStorage.setItem("draft", JSON.stringify({ title: "WIP" }));
const draft = JSON.parse(sessionStorage.getItem("draft"));
// Both only store strings — serialize objects manually
localStorage.setItem("user", JSON.stringify({ id: 1, name: "Ana" }));
const user = JSON.parse(localStorage.getItem("user"));
setTimeout runs the callback once after a delay. setInterval runs it repeatedly at fixed intervals. Both return an ID for cancellation. Use recursive setTimeout when execution time varies — setInterval fires regardless of whether the previous run has finished, which can cause overlap.
// setInterval — fires every 1s even if work takes longer
const id = setInterval(() => fetchStats(), 1000);
clearInterval(id); // cancel
// Recursive setTimeout — next tick starts only after work finishes
function poll() {
fetchStats().then(data => {
render(data);
setTimeout(poll, 1000); // schedule AFTER completion ✓
});
}
poll();
A pure function always returns the same output for the same inputs, and produces no side effects — it does not modify external state, perform I/O, or mutate its arguments. Pure functions are easy to test, cache (memoize), and reason about because their behavior is fully predictable from their arguments alone.
// ❌ Impure — mutates external state
let total = 0;
function addToTotal(n) { total += n; }
// ❌ Impure — depends on external state
function getDiscount(price) {
return price * currentUser.discountRate; // side dependency
}
// ✓ Pure — same input → same output, no side effects
function add(a, b) { return a + b; }
function applyDiscount(price, rate) {
return price * (1 - rate); // all inputs explicit
}
// ✓ Pure array operations (return new arrays)
const doubled = [1, 2, 3].map(x => x * 2); // [2, 4, 6]
Currying transforms a function that takes multiple arguments into a sequence of functions each taking a single argument. It enables partial application — locking in some arguments upfront to produce specialized, reusable functions without repetition.
// Regular function
const multiply = (a, b) => a * b;
// Curried version
const curriedMultiply = a => b => a * b;
const double = curriedMultiply(2); // partial application
const triple = curriedMultiply(3);
double(5); // 10
triple(5); // 15
// Practical example — reusable formatters
const formatCurrency = currency => amount =>
new Intl.NumberFormat("en-US", { style: "currency", currency })
.format(amount);
const formatUSD = formatCurrency("USD");
const formatEUR = formatCurrency("EUR");
formatUSD(1234.5); // "$1,234.50"
formatEUR(1234.5); // "€1,234.50"
Function composition combines two or more functions so that the output of one becomes the input of the next. This builds complex transformations from small, focused, testable pieces. pipe applies functions left-to-right; compose applies right-to-left.
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const trim = s => s.trim();
const toLowerCase = s => s.toLowerCase();
const slugify = s => s.replace(/\s+/g, "-");
const toSlug = pipe(trim, toLowerCase, slugify);
toSlug(" Hello World "); // "hello-world"
// Each step is independently testable ✓
// Add / remove steps without touching others ✓
A Proxy wraps an object and intercepts fundamental operations — property reads, writes, function calls, and more — via traps. This enables validation, logging, auto-defaults, reactive systems (like Vue 3's reactivity), and access control without modifying the original object.
function createValidator(target, schema) {
return new Proxy(target, {
set(obj, prop, value) {
if (schema[prop] && typeof value !== schema[prop]) {
throw new TypeError(`${prop} must be a ${schema[prop]}`);
}
obj[prop] = value;
return true;
}
});
}
const user = createValidator({}, { name: "string", age: "number" });
user.name = "Alice"; // ✓
user.age = 30; // ✓
user.age = "thirty"; // TypeError: age must be a number ⚠️
Object.freeze() prevents adding, removing, and changing properties — the object becomes fully immutable at the top level. Object.seal() allows changing existing property values but prevents adding or removing properties. Neither operates deeply on nested objects.
// freeze — nothing can change
const config = Object.freeze({ host: "localhost", port: 3000 });
config.port = 8080; // silently ignored (TypeError in strict mode)
config.debug = true; // silently ignored
console.log(config.port); // 3000
// seal — values changeable, structure locked
const settings = Object.seal({ theme: "dark", lang: "en" });
settings.theme = "light"; // ✓ changing existing value
settings.zoom = 1.5; // ✗ ignored — can't add new keys
console.log(Object.isSealed(settings)); // true
console.log(Object.isFrozen(config)); // true
All three return arrays of an object's own enumerable string-keyed properties, excluding inherited ones. keys() returns property names, values() returns property values, and entries() returns [key, value] pairs. entries() is particularly useful for converting objects to Maps or iterating with both key and value.
const scores = { alice: 92, bob: 85, carol: 97 };
Object.keys(scores); // ["alice", "bob", "carol"]
Object.values(scores); // [92, 85, 97]
Object.entries(scores); // [["alice",92], ["bob",85], ["carol",97]]
// Find top scorer
const top = Object.entries(scores)
.sort(([, a], [, b]) => b - a)[0];
console.log(`${top[0]}: ${top[1]}`); // "carol: 97"
// Convert object to Map
const map = new Map(Object.entries(scores));
// Rebuild object from entries (transform values)
const curved = Object.fromEntries(
Object.entries(scores).map(([k, v]) => [k, Math.min(v + 5, 100)])
);
A tagged template prefixes a template literal with a function. That function receives the static string parts and the interpolated values as separate arguments, giving full control over how the result is assembled. Common uses include SQL query builders, HTML sanitizers, internationalization (i18n), and styled-components in React.
// Safe HTML escaping tag
function html(strings, ...values) {
const escape = s => String(s)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
return strings.reduce((result, str, i) =>
result + str + (values[i] !== undefined ? escape(values[i]) : ""), "");
}
const userInput = '<script>alert("xss")</script>';
const safeHTML = html`Hello, ${userInput}!
`;
// Hello, <script>alert("xss")</script>!
✓
flatMap() runs a mapping function on each element and then flattens the result by one level. It is equivalent to .map(...).flat(1) but more efficient in a single pass. Use it whenever each element can produce zero, one, or many output items — for example, splitting words into characters, or expanding a dataset.
// map alone creates nested arrays
["Hello", "World"].map(w => w.split(""));
// [["H","e","l","l","o"], ["W","o","r","l","d"]]
// flatMap flattens one level automatically
["Hello", "World"].flatMap(w => w.split(""));
// ["H","e","l","l","o","W","o","r","l","d"]
// Practical: expand orders into line items
const orders = [
{ id: 1, items: ["book", "pen"] },
{ id: 2, items: ["laptop"] },
];
orders.flatMap(o => o.items); // ["book", "pen", "laptop"]
// Return [] to filter items out, value to keep
const parsed = ["1", "two", "3"].flatMap(s => {
const n = Number(s);
return isNaN(n) ? [] : [n]; // skip non-numbers
});
// [1, 3]
.at() accepts negative indices, where -1 means the last element, -2 the second-to-last, and so on. With bracket notation you have to write arr[arr.length - 1] every time. .at() also works on strings and typed arrays.
const scores = [10, 20, 30, 40, 50];
// Old way
scores[scores.length - 1]; // 50
scores[scores.length - 2]; // 40
// at() — clean and readable
scores.at(-1); // 50
scores.at(-2); // 40
scores.at(0); // 10 (positive index also works)
// Works on strings too
"hello".at(-1); // "o"
// Chaining without saving to a variable
[1, 2, 3].map(x => x * 2).at(-1); // 6
Array.from() accepts any iterable or array-like object and converts it to a real array. Passing { length: N } as the first argument creates a sparse-free array of N slots. The optional second argument is a mapping function applied to each index — making it a concise range generator.
// Generate a range [0..4]
Array.from({ length: 5 }, (_, i) => i);
// [0, 1, 2, 3, 4]
// Generate [1..10]
Array.from({ length: 10 }, (_, i) => i + 1);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Convert NodeList to array (DOM)
const divs = Array.from(document.querySelectorAll("div"));
divs.filter(el => el.classList.contains("active"));
// Convert Set to array
Array.from(new Set([1, 2, 2, 3])); // [1, 2, 3]
findLast() searches an array from right to left and returns the last element that satisfies the predicate. findLastIndex() does the same but returns the index instead of the value. They save you from reversing the array or writing workarounds when you need the last match.
const logs = [
{ level: "info", msg: "started" },
{ level: "error", msg: "timeout" },
{ level: "info", msg: "retrying" },
{ level: "error", msg: "failed" },
];
// Last error — no reverse() needed
logs.findLast(l => l.level === "error");
// { level: "error", msg: "failed" }
logs.findLastIndex(l => l.level === "error");
// 3
// Old workaround (avoid this)
[...logs].reverse().find(l => l.level === "error");
Logical assignment combines a logical operator with assignment, but only assigns when the condition would act. x &&= y assigns only if x is truthy. x ||= y assigns only if x is falsy. x ??= y assigns only if x is null or undefined. They short-circuit — the right side is never evaluated if the condition isn't met.
// &&= — update only if property exists/truthy
user.name &&= user.name.trim();
// same as: if (user.name) user.name = user.name.trim();
// ||= — provide default for falsy values
settings.theme ||= "light";
// same as: settings.theme = settings.theme || "light";
// ??= — provide default only for null/undefined
config.retries ??= 3;
// same as: config.retries = config.retries ?? 3;
// NOTE: config.retries = 0 would NOT be overwritten by ??= ✓
&& stops and returns the first falsy value; if all are truthy it returns the last value. || stops and returns the first truthy value. This means you can use them for conditional execution, default values, and guarded function calls — without writing a full if statement.
// && — run only if condition is truthy
isLoggedIn && renderDashboard();
// || — first truthy wins (default values)
const port = process.env.PORT || 3000;
// Conditional rendering pattern (common in React JSX)
const ui = isLoading && "<Spinner />";
// Chained guard
user && user.profile && user.profile.avatar && showAvatar(user.profile.avatar);
// cleaner with optional chaining:
user?.profile?.avatar && showAvatar(user.profile.avatar);
ES6 destructuring makes swapping a one-liner. The right-hand side is evaluated first as an array, then unpacked into the left-hand side variables simultaneously — no temporary variable needed. This also works for swapping elements inside an array.
// Old way — temp variable
let a = 1, b = 2;
let tmp = a; a = b; b = tmp;
// ES6 destructuring swap
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
// Swap elements inside an array
const arr = [10, 20, 30, 40];
[arr[0], arr[3]] = [arr[3], arr[0]];
console.log(arr); // [40, 20, 30, 10]
Promise.allSettled() waits for all promises to finish — fulfilled or rejected — and returns an array of result objects with a status field. Unlike Promise.all(), it never short-circuits on failure. Use it when you need to process every result regardless of whether some failed.
const requests = [
fetch("/api/users"),
fetch("/api/posts"),
fetch("/api/broken-endpoint"),
];
// Promise.all — throws on first failure, others ignored
// Promise.allSettled — waits for all ✓
const results = await Promise.allSettled(requests);
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("OK:", result.value);
} else {
console.warn("Failed:", result.reason);
}
});
// Extract only successful ones
const ok = results
.filter(r => r.status === "fulfilled")
.map(r => r.value);
AbortController provides a signal you pass to fetch. Calling .abort() on the controller cancels the request and causes the fetch promise to reject with an AbortError. This is essential for preventing stale results in search-as-you-type UIs and cleaning up on component unmount.
async function searchUsers(query) {
const controller = new AbortController();
// Cancel after 5 seconds
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(`/api/users?q=${query}`, {
signal: controller.signal,
});
clearTimeout(timeout);
return await res.json();
} catch (err) {
if (err.name === "AbortError") {
console.log("Request was cancelled");
} else {
throw err;
}
}
}
// Cancel previous search on new input (React pattern)
useEffect(() => {
const controller = new AbortController();
fetchData(query, controller.signal);
return () => controller.abort(); // cleanup ✓
}, [query]);
queueMicrotask() schedules a callback in the microtask queue, which drains before any macrotask (including setTimeout). Use it when you need to defer work until after the current synchronous execution finishes, but before the browser renders or runs timers — for example, batching DOM updates.
console.log("1 – sync start");
setTimeout(() => console.log("4 – macrotask"), 0);
queueMicrotask(() => console.log("3 – microtask"));
Promise.resolve().then(() => console.log("2 – promise microtask"));
console.log("1 – sync end");
// Output order:
// 1 – sync start
// 1 – sync end
// 2 – promise microtask (microtask queue)
// 3 – microtask (microtask queue)
// 4 – macrotask (macrotask queue — runs last)
Property shorthand lets you omit the value when a variable has the same name as the desired key. Computed property names let you use any expression inside [] as a key at object creation time. Together they enable highly dynamic, readable object construction.
const name = "Alice";
const age = 30;
// Shorthand — no need to write { name: name, age: age }
const user = { name, age }; // { name: "Alice", age: 30 }
// Computed property names
const field = "email";
const profile = { [field]: "alice@example.com" };
// { email: "alice@example.com" }
// Dynamic key from expression
const prefix = "get";
const api = {
[`${prefix}User`]: () => fetch("/user"),
[`${prefix}Orders`]: () => fetch("/orders"),
};
api.getUser();
Object.hasOwn(obj, key) is a static method that safely checks whether an object has a property as its own (not inherited). obj.hasOwnProperty() can fail if the object has a null prototype (created with Object.create(null)) or if the property hasOwnProperty has been overridden — Object.hasOwn() avoids both pitfalls.
// Dangerous — can be shadowed
const obj1 = { hasOwnProperty: () => false };
obj1.hasOwnProperty("key"); // false ⚠️ (method was overridden)
// Dangerous — null-prototype objects have no method
const obj2 = Object.create(null);
obj2.key = "value";
obj2.hasOwnProperty("key"); // TypeError ⚠️
// Safe in both cases ✓
Object.hasOwn(obj1, "hasOwnProperty"); // true
Object.hasOwn(obj2, "key"); // true
// Common use: guard before accessing a property
if (Object.hasOwn(config, "timeout")) {
applyTimeout(config.timeout);
}
padStart(targetLength, fillString) pads the beginning of a string until it reaches the target length. padEnd() does the same at the end. Both are non-destructive and return a new string. They shine for formatting numbers, aligning table columns, and building fixed-width identifiers.
// Zero-pad invoice numbers
String(42).padStart(6, "0"); // "000042"
String(1234).padStart(6, "0"); // "001234"
// Align a table
const rows = [["Alice", "92"], ["Bob", "8"], ["Carol", "100"]];
rows.forEach(([name, score]) => {
console.log(name.padEnd(10) + score.padStart(4));
});
// "Alice 92"
// "Bob 8"
// "Carol 100"
// Mask sensitive data
const card = "4111111111111234";
const masked = card.slice(-4).padStart(card.length, "*");
// "************1234"
IntersectionObserver fires a callback when a target element enters or leaves the viewport (or a specified root). It replaces expensive scroll event listeners with getBoundingClientRect() polling. Common uses: lazy-loading images, infinite scroll, animation triggers on scroll, and ad visibility tracking.
// Lazy-load images when they enter the viewport
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // load actual image
observer.unobserve(img); // stop watching once loaded
}
});
}, { threshold: 0.1 }); // trigger at 10% visibility
document.querySelectorAll("img[data-src]")
.forEach(img => observer.observe(img));
ResizeObserver notifies you whenever a specific element's size changes — not the window, but any DOM element. This is essential for component-level responsive behaviour (charts, canvas, text overflow), replacing fragile window resize listeners that don't catch container size changes caused by layout shifts.
const chart = document.getElementById("chart");
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`Chart is now ${width}×${height}px`);
redrawChart(width, height); // re-render on every size change
}
});
ro.observe(chart);
// Stop watching
ro.unobserve(chart);
// or disconnect all observations:
ro.disconnect();
MutationObserver watches for changes in the DOM tree — added/removed nodes, attribute changes, and text content changes. It is the modern replacement for the deprecated DOM mutation events. Use it to react to third-party DOM changes, implement undo history, or detect when a specific element appears.
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(node => {
if (node.matches?.(".ad-banner")) {
node.remove(); // auto-remove injected elements
}
});
}
if (mutation.type === "attributes") {
console.log(`Attr "${mutation.attributeName}" changed`);
}
});
});
observer.observe(document.body, {
childList: true, // watch for added/removed children
subtree: true, // include all descendants
attributes: true, // watch attribute changes
});
observer.disconnect(); // stop when done
Intermediate & Advanced Topics
When you call a function with new, JavaScript performs four steps automatically: (1) creates a fresh empty object, (2) links that object's [[Prototype]] to the constructor's prototype, (3) runs the constructor with this pointing to the new object, (4) returns the new object — unless the constructor explicitly returns a different object.
function Person(name, age) {
// 'this' is the newly created object
this.name = name;
this.age = age;
}
Person.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
const alice = new Person("Alice", 30);
alice.greet(); // "Hi, I'm Alice"
alice instanceof Person; // true
Object.getPrototypeOf(alice) === Person.prototype; // true
// What new does internally (manual equivalent)
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype); // steps 1-2
const result = Constructor.apply(obj, args); // step 3
return result instanceof Object ? result : obj; // step 4
}
Object.create(proto) creates a new empty object whose [[Prototype]] is set to proto — with no constructor call involved. Pass null to create a truly blank object with no prototype at all (no toString, no hasOwnProperty). It is the most direct way to set up prototype delegation without needing classes or constructor functions.
const animal = {
describe() { return `I am a ${this.type}`; }
};
// dog inherits from animal — no constructor needed
const dog = Object.create(animal);
dog.type = "dog";
dog.describe(); // "I am a dog"
Object.getPrototypeOf(dog) === animal; // true
// Null prototype — pure dictionary, no inherited methods
const dict = Object.create(null);
dict.key = "value";
// dict.toString → undefined (intentionally, no prototype) ✓
// Useful for caches/maps where inherited keys could cause bugs
In non-arrow functions, arguments is an array-like object (not a real array) containing all passed arguments, even undeclared ones. It is outdated: it does not work in arrow functions, has no array methods, and makes intent unclear. Always prefer rest parameters (...args), which are real arrays and work everywhere.
// Old way — arguments object
function sumOld() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
sumOld(1, 2, 3); // 6
// ❌ arguments is array-like, not a real array
arguments.map // undefined — no array methods!
arguments.reduce // undefined
// ✓ Modern — rest parameters are a real Array
function sumNew(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sumNew(1, 2, 3, 4); // 10
nums.map(n => n * 2); // works ✓
typeof returns a string for primitive types and "object" for most reference types — it cannot distinguish between an array, a Date, or a plain object. instanceof walks the prototype chain to check whether an object was created by a specific constructor — it works across class hierarchies but fails across iframes.
// typeof — good for primitives
typeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ← bug!
typeof [] // "object" ← can't distinguish!
typeof {} // "object"
typeof function(){} // "function" ← special case
// instanceof — good for objects
[] instanceof Array // true
[] instanceof Object // true (Array inherits from Object)
new Date() instanceof Date // true
// Best-in-class type detection
Array.isArray([]) // true ← use for arrays
Object.prototype.toString.call(new Date()) // "[object Date]"
Object.prototype.toString.call([]) // "[object Array]"
When a variable is declared in an inner scope with the same name as one in an outer scope, the inner variable shadows the outer one within that scope. JavaScript resolves variable names by walking up the scope chain from the innermost scope outward until it finds a match or reaches the global scope.
let message = "global";
function outer() {
let message = "outer"; // shadows global
function inner() {
let message = "inner"; // shadows outer
console.log(message); // "inner"
}
inner();
console.log(message); // "outer"
}
outer();
console.log(message); // "global"
// Accidental shadowing is a common bug source
function processUser(user) {
if (user.isAdmin) {
const user = getAdminData(); // ← shadows param! bug-prone
console.log(user);
}
console.log(user); // original user — not affected in this case
}
Object.defineProperty() adds or modifies a property with fine-grained control via a descriptor. You can make a property non-writable (read-only), non-enumerable (hidden from loops and JSON.stringify), and non-configurable (can't be deleted or re-defined). This is the low-level mechanism that Object.freeze() uses internally.
const config = {};
Object.defineProperty(config, "API_URL", {
value: "https://api.example.com",
writable: false, // cannot be reassigned
enumerable: false, // hidden from for...in / Object.keys
configurable: false, // cannot be deleted or redefined
});
config.API_URL = "https://evil.com"; // silently fails (strict → TypeError)
console.log(config.API_URL); // "https://api.example.com" — unchanged ✓
Object.keys(config); // [] — not enumerable, hidden ✓
// Multiple at once
Object.defineProperties(config, {
VERSION: { value: "2.0", writable: false, enumerable: true },
DEBUG: { value: false, writable: true, enumerable: true },
});
[[Prototype]] is the internal hidden link every object has — it is what the engine walks when looking up a missing property. .prototype is a plain property on function objects that becomes the [[Prototype]] of objects created with new. __proto__ is a legacy accessor that exposes [[Prototype]] — use Object.getPrototypeOf() instead.
function Dog(name) { this.name = name; }
Dog.prototype.bark = function () { return "Woof!"; };
const rex = new Dog("Rex");
// .prototype — property on the constructor function
Dog.prototype; // { bark: f, constructor: Dog }
// [[Prototype]] of rex — exposed via getPrototypeOf
Object.getPrototypeOf(rex) === Dog.prototype; // true
// __proto__ — legacy, deprecated, avoid
rex.__proto__ === Dog.prototype; // true (but don't use this)
// Lookup chain for rex.bark:
// rex → Dog.prototype → Object.prototype → null
rex.bark(); // "Woof!" (found on Dog.prototype)
rex.toString(); // found on Object.prototype
Classical inheritance (Java, C++) copies behavior from a parent class into child classes at compile time — the relationship is rigid. JavaScript's prototypal delegation is a live link: objects delegate to other objects at runtime. Adding a method to a prototype instantly makes it available on all existing objects that delegate to it, with no copying involved.
const vehicle = {
describe() { return `A ${this.type} going ${this.speed} km/h`; }
};
const car = Object.create(vehicle);
car.type = "car";
car.speed = 120;
const bike = Object.create(vehicle);
bike.type = "bike";
bike.speed = 30;
car.describe(); // "A car going 120 km/h"
bike.describe(); // "A bike going 30 km/h"
// Add to the prototype AFTER objects were created — they get it instantly
vehicle.stop = function () { return `${this.type} stopped.`; };
car.stop(); // "car stopped." ← method wasn't there when car was created ✓
The Revealing Module Pattern uses an IIFE to create a private scope and returns only the public API, "revealing" selected names. It solved the global-namespace pollution problem before native modules existed. ES Modules formalized this idea — each file has its own scope, and only exported names are accessible from outside.
// Revealing Module Pattern (pre-ES6)
const CartModule = (() => {
let items = []; // private
function add(item) { items.push(item); }
function remove(item) { items = items.filter(i => i !== item); }
function total() { return items.reduce((s, i) => s + i.price, 0); }
return { add, remove, total }; // reveal public API only
})();
CartModule.add({ name: "Book", price: 12 });
CartModule.items; // undefined — private ✓
// Modern ES Module equivalent (cart.js)
let items = [];
export const add = item => items.push(item);
export const remove = item => items = items.filter(i => i !== item);
export const total = () => items.reduce((s, i) => s + i.price, 0);
Modern JavaScript engines use mark-and-sweep: starting from root objects (global, current stack frames), the GC marks everything reachable, then sweeps away everything unmarked. An object is collected when no reachable reference points to it — not when a reference counter hits zero (which fails with circular references). This is why closures, detached DOM nodes, and forgotten event listeners can cause leaks.
// ✓ Eligible for GC — nothing references it after function exits
function create() {
let data = new Array(100_000).fill(0);
return data.reduce((s, n) => s + n, 0);
}
create(); // data is collected after function returns
// ❌ Leak — closure keeps data alive indefinitely
let leak;
function setup() {
const data = new Array(100_000).fill(0);
leak = () => data.length; // leak holds a reference to data forever
}
// ❌ Leak — detached DOM node kept in memory
let detached = document.getElementById("removed-element");
document.body.removeChild(detached);
// detached variable still holds the node — GC can't free it
// ✓ Fix
detached = null; // release the reference