JavaScript Guide

The language of the web, end to end β€” variables, functions, arrays, objects, the DOM, events, promises, async/await, fetch, and modules. Every code block has a Copy button.

Copy button on each code block  Β·  ● Beginner ● Intermediate ● Advanced

01 Introduction β€” The Language of the Web

JavaScript (JS) is the only programming language that runs natively in every web browser. It adds behaviour to a page: what happens when you click, what appears after a request, how data is validated, how the UI updates. If HTML is the skeleton and CSS is the skin, JavaScript is the muscle and nervous system.

What makes JavaScript different

Where JavaScript runs

Browsers
V8 (Chrome/Edge), SpiderMonkey (Firefox), JavaScriptCore (Safari)
Node.js
Servers, CLIs, scripts β€” V8 outside the browser
Deno / Bun
Modern JS/TS runtimes with batteries included
Desktop / Mobile
Electron apps, React Native apps

A very short history

Analogy: HTML decides what is on the page, CSS decides how it looks, JavaScript decides what it does. A page can work with zero JS β€” but every interactive product (logins, carts, feeds, maps) needs it.
πŸ’‘ Tip: Learn vanilla JavaScript deeply before any framework. React, Vue and Angular are wrappers around the same DOM, events and async concepts covered in this guide β€” frameworks change every few years, these fundamentals do not.

02 Setup & Dev Tools Beginner

Three ways to run JavaScript

<!-- 1. External file (recommended β€” always) -->
<script src="app.js" defer></script>

<!-- 2. Inline block (small sites, demos only) -->
<script>
  console.log('Hello from the page');
</script>

<!-- 3. Inline attribute (avoid β€” mixing behaviour into HTML) -->
<button onclick="alert('hi')">Click</button>
// app.js
const button = document.querySelector('#save');
button.addEventListener('click', () => {
  console.log('Saved!');
});

defer vs async

<script src>
Blocks HTML parsing until the file loads
defer
Downloads in parallel, runs AFTER the DOM is ready, in order β€” use this
async
Downloads in parallel, runs as soon as it arrives β€” order not guaranteed (analytics)
end of <body>
Old-school approach β€” works, but defer is cleaner
⚠ Common beginner bug: a script in <head> without defer runs before the HTML below it exists β€” querySelector returns null and you get "Cannot read properties of null". Always add defer (or put the script at the end of <body>).

Console methods you'll actually use

console.log('plain', user);          // multiple values, one group
console.warn('deprecated');        // yellow warning
console.error('failed');            // red error + stack trace
console.table(users);                 // rendered table for arrays/objects
console.dir(user);                    // live object, expandable
console.group('Step 1');             // nested groups
console.groupEnd();
console.time('load'); … console.timeEnd('load');  // timing
console.assert(user, 'user is required');   // logs only when falsy
console.trace();                      // where was this called from?

Browser DevTools β€” your superpower

Console
Run JS live, inspect logged objects
Sources β†’ breakpoints
Click a line number; execution pauses with full scope inspection
Conditional breakpoint
Right-click a breakpoint β†’ add a condition (e.g. id === 5)
Elements β†’ Event Listeners
See every listener attached to an element
Network
Inspect fetch/XHR requests, status codes, payloads
Ctrl/Cmd + P
Jump to any file in Sources, then set breakpoints

Node.js β€” JavaScript outside the browser

node -v                 # check installed version
node app.js             # run a file
node                    # REPL β€” evaluate JS interactively

npm init -y             # create package.json
npm install express     # add a package (creates node_modules)
npm install -D nodemon  # dev-only dependency
Node vs browser: the core language is identical. Node adds fs, http, process β€” and has no document or window. Browser code has the DOM β€” and no file system.
✍ Try it

Create index.html + app.js (with defer), log document.title in the console, then set a breakpoint on that line and reload. Inspect what's in scope.

03 Variables & Data Types Beginner

let, const, var

const siteName = 'Laksh Guides';   // block-scoped, cannot be reassigned β€” DEFAULT
let theme = 'dark';               // block-scoped, will be reassigned
var old = 'legacy';               // function-scoped, hoisted, ambiguous β€” avoid

theme = 'light';                   // OK β€” let allows reassignment
// siteName = 'Other';             // TypeError: assignment to constant
const user = { name: 'Laksh' };
user.name = 'Ron';        // allowed β€” mutating the object
// user = {};              // NOT allowed β€” reassigning the constant
Block scope: let and const exist only inside the nearest { } β€” loops, ifs, functions. This prevents the classic "loop variable leaked everywhere" bug that var had.

Primitive types (7)

string
'hello', "hi", `tpl` β€” text
number
42, 3.14, NaN, Infinity β€” binary doubles
bigint
9007199254740991n β€” integer beyond 2⁡³-1
boolean
true / false
undefined
declared but no value β€” the default
null
intentional "no value" β€” you assign it
symbol
unique identifier, rarely needed in app code
object
everything else β€” arrays, functions, dates, null!
typeof 'hi'          // "string"
typeof 42            // "number"
typeof true          // "boolean"
typeof undefined     // "undefined"
typeof null          // "object"   ← historical bug, always was
typeof []            // "object"   ← arrays are objects
typeof () => {}      // "function"

Array.isArray([])     // true β€” the correct array check
typeof NaN           // "number"   ← NaN = Not a Number, still a number type

Dynamic typing & coercion

let x = 1;        // number
x = 'one';      // now a string β€” no error

// == coerces types before comparing (avoid it)
5 == '5'       // true
0 == ''        // true  ← surprise!

// === compares type AND value (always use this)
5 === '5'      // false
5 === 5        // true
⚠ Golden rule: always use === and !==. The only time == is safe is x == null, which checks null or undefined in one shot β€” everything else should be strict.

Converting values

// To string
String(42)              // "42"
(42).toString()       // "42"
`42`                  // "42"

// To number
Number('42')            // 42
'42' * 1                // 42 β€” trick used in old code
parseInt('42px', 10)  // 42 β€” stop at first non-digit (pass radix 10)
parseFloat('3.5m')    // 3.5
Number('abc')           // NaN

// To boolean β€” only 8 falsy values exist
Boolean(0)              // false
Boolean('')             // false
Boolean(null)           // false
Boolean(undefined)      // false
Boolean(NaN)            // false
Boolean('0')            // true β€” non-empty string!
πŸ’‘ Tip: 0.1 + 0.2 === 0.3 is false (0.30000000000000004) β€” all languages with binary floating point share this. For money, store paise/cents as integers (β‚Ή1 = 100) or use a decimal library. Compare floats with Math.abs(a - b) < 0.0001, never with ===.

04 Operators & Control Flow Beginner

Operators at a glance

// Arithmetic
+  -  *  /  %  **
%  = modulo (remainder) β€” "is even?" β†’  n % 2 === 0
** = exponent        β†’  2 ** 10 === 1024
+= -= *= /=          β†’  count += 1
++ --                 β†’  count++ (avoid inside expressions)

// Comparison
=== !==  // strict β€” ALWAYS use these
< > <= >=

// Logical
&&  both must be true      β†’  isAdult && hasId
||  at least one true      β†’  isAdmin || isOwner
!   negation              β†’  !isLoading
??  nullish coalescing     β†’  nickname ?? name   // only null/undefined
?.  optional chaining      β†’  user?.address?.city

Truthiness β€” the 8 falsy values

Everything in JavaScript is truthy except these eight:

false
the boolean
0, -0, 0n
zero (any form)
''
empty string
null
intentional empty
undefined
no value assigned
NaN
not-a-number
⚠ Surprises to remember: '0', 'false' and [] (empty array) are truthy β€” they are non-empty values. So if (value) on the string '0' passes. To check "has content", be explicit: value.length > 0.

Conditionals

if (score >= 90) {
  grade = 'A';
} else if (score >= 80) {
  grade = 'B';
} else {
  grade = 'C';
}

// Ternary β€” one-line if/else (readable only when short)
const label = isLoggedIn ? 'Dashboard' : 'Login';

// switch β€” remember break (or it "falls through")
switch (day) {
  case 'Sat':
  case 'Sun':
    isWeekend = true;
    break;
  case 'Mon':
    isWeekend = false;
    break;
  default:
    isWeekend = false;
}
πŸ’‘ Tip: for 3+ related branches a switch or a lookup object is cleaner than nested else if: const icons = { sun:'β˜€οΈ', moon:'πŸŒ™' }; icons[day] ?? '❓';

Loops

// Classic for β€” when you need the index or custom stepping
for (let i = 0; i < items.length; i++) { console.log(i, items[i]); }

// while β€” repeats until condition turns false
while (attempts < 3) { attempts++; }

// for...of β€” VALUES of any iterable (arrays, strings, maps) β€” PREFERRED
for (const item of items) { console.log(item); }
for (const [i, item] of items.entries()) { console.log(i, item); }

// for...in β€” KEYS of an object (works on arrays too β€” don't use it there)
for (const key in user) { console.log(key, user[key]); }

// Control
for (const n of numbers) {
  if (n % 2 !== 0) continue;   // skip odds
  if (n > 100) break;          // stop entirely
}
⚠ Never use for...in on arrays. It iterates string keys ('0', '1') and can pick up inherited properties. Use for...of, forEach or a classic for loop instead.
✍ Try it

Print a multiplication table (1–10) for a number using a nested loop, then rewrite it with Array.from and map when you reach the Arrays section. Notice how the data-flow thinking changes.

05 Functions, Scope & Closures Intermediate

A function is a reusable block of behaviour. In JavaScript, functions are values β€” you can store them in variables, pass them to other functions and return them from functions.

Four ways to write one

// 1. Declaration β€” hoisted (usable before its line)
function add(a, b) { return a + b; }

// 2. Function expression β€” created at runtime
const mul = function (a, b) { return a * b; };

// 3. Arrow function β€” concise, lexical `this` (ideal for callbacks)
const sub = (a, b) => a - b;
const square = n => n * n;              // single param: no parens
const noop = () => {};                   // no params: empty parens required

// 4. Async function β€” always returns a promise
async function loadUser(id) { return fetchUser(id); }
⚠ Arrow gotcha: a single expression body returns implicitly β€” n => n * n. To return an object literal, wrap it: () => ({ name: 'Laksh' }). Missing braces here is the #1 arrow-function bug.

Parameters β€” defaults and rest

function greet(name = 'friend', greeting = 'Hi') {
  return `${greeting}, ${name}!`;
}
greet()                  // "Hi, friend!"
greet('Laksh')            // "Hi, Laksh!"

// Rest β€” collects remaining arguments into a real array
function max(...nums) {
  return nums.reduce((a, b) => (b > a ? b : a), -Infinity);
}
max(3, 9, 4)            // 9

Scope & closures

Scope = where a variable is visible. Inner scopes can read outer variables; outer scopes cannot see inside. A closure is a function that remembers the variables from the place it was created β€” even after that place has finished running.

function createCounter() {
  let count = 0;                      // private β€” hidden from outside
  return {
    inc:  () => ++count,
    get:  () => count,
    reset: () => { count = 0; }
  };
}

const counter = createCounter();
counter.inc();   // 1
counter.inc();   // 2
counter.get();   // 2
// count is NOT accessible here β€” the closure protects it

Functions as values (callbacks)

const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2);   // [2,4,6,8] β€” a function passed in

button.addEventListener('click', () => {     // function given to another
  console.log('clicked');
});                                        // block of code, called later by JS

// Function factory via closure
function multiplyBy(factor) {
  return n => n * factor;
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
double(5)   // 10
this in 30 seconds: regular functions get this from how they are called (obj.fn() β†’ obj). Arrow functions have no this of their own β€” they inherit it from the surrounding function. That's why arrows are perfect for callbacks inside methods.

06 Arrays & Array Methods Intermediate

const fruits = ['apple', 'banana', 'mango'];
const nums   = [10, 20, 30];
const mixed  = [1, 'two', true, { id: 1 }, []];  // JS arrays can hold anything

fruits.length       // 3
fruits[0]           // "apple" β€” indexes start at 0
fruits.at(-1)       // "mango" β€” negative index from the end
fruits[fruits.length - 1]  // "mango" β€” old way

Transforming β€” map, filter, reduce

const users = [
  { name: 'Laksh', age: 20, active: true },
  { name: 'Asha',  age: 23, active: false },
  { name: 'Rahul', age: 19, active: true },
];

users.map(u => u.name)        // ["Laksh","Asha","Rahul"] β€” transform, same length
users.filter(u => u.active)   // 2 users                  β€” keep matches
users.reduce((sum, u) => sum + u.age, 0)  // 62 β€” fold into one value

users.find(u => u.age < 21)    // {Laksh…} β€” first match or undefined
users.findIndex(u => u.name === 'Asha')  // 1
users.some(u => !u.active)    // true  β€” at least one
users.every(u => u.age >= 18) // true  β€” all of them
The golden trio: map changes every item, filter removes items, reduce squashes everything into a single value (sum, object, count). Master these three and 90% of array problems shrink to one line.

Adding, removing, rearranging

push / pop
Add/remove at the END β€” mutates
shift / unshift
Remove/add at the START β€” mutates, slow
splice(i, n, ...)
Remove/insert at index β€” mutates, returns removed
slice(a, b)
Copy a range [a, b) β€” never mutates
concat / [...arr]
Combine arrays β€” new array
sort(fn)
Sort β€” MUTATES, defaults to text order
reverse()
Reverse β€” mutates (use [...arr].reverse())
flat(depth)
Flatten nested arrays β€” [[1],[2]] β†’ [1,2]
// ⚠ sort() without a comparator sorts LEXICOGRAPHICALLY (as text)
[10, 9, 100].sort()                 // [10, 100, 9]  ← wrong
[10, 9, 100].sort((a, b) => a - b)  // [9, 10, 100]  ← numeric

// Sorting objects
users.sort((a, b) => a.age - b.age);                  // ascending
users.sort((a, b) => a.name.localeCompare(b.name));   // alphabetical

Iteration styles

items.forEach((item, i) => console.log(i, item));  // no return value

for (const item of items) { … }                  // supports break/continue

items
  .filter(u => u.active)
  .map(u => u.name)
  .sort();                                        // chaining reads like a pipeline
⚠ Mutating vs pure: push, pop, shift, unshift, splice, sort, reverse, fill change the original array β€” dangerous when that array is state shared with a UI (React/Vue). For immutable code, copy first: [...arr].sort().
πŸ’‘ Tip: forEach cannot break out early β€” use a plain for...of, or the trick arr.some(x => { …; return true; }) for an early exit.

07 Objects, Spread & Destructuring Intermediate

Objects are collections of key: value pairs β€” the main data structure for everything shaped (a user, a product, a config, API responses).

const user = {
  name: 'Laksh',              // shorthand: same as name: name
  age: 20,
  email: 'laksh@example.com',
  active: true,
  tags: ['dev', 'student'],
  address: { city: 'Mangalore', pin: 575001 },

  // method β€” `this` refers to this object
  greet() { return `Hi, I'm ${this.name}`; },

  ['computed_' + 'key']: true,   // computed key
};

user.name            // dot access (keys must be valid identifiers)
user['email']         // bracket access β€” also for dynamic keys
const field = 'age';
user[field]          // 20

user.city = 'Udupi'; // add
delete user.active;   // remove
'name' in user      // true  β€” key exists check
Object.hasOwn(user, 'name')  // true β€” safer (ignores prototype chain)

Iterating over objects

Object.keys(user)     // ["name","age",…]   β€” array of keys
Object.values(user)   // ["Laksh",20,…]     β€” array of values
Object.entries(user)  // [["name","Laksh"],…] β€” [key, value] pairs

for (const [key, value] of Object.entries(user)) {
  console.log(key, 'β†’', value);
}

// Or directly
for (const key in user) { console.log(key, user[key]); }

Destructuring β€” unpack values

const { name, age, role = 'student' } = user;   // default if undefined
const { name: fullName } = user;                 // rename
const { address: { city } } = user;              // nested
const { password, ...safeUser } = user;          // rest β€” omit one

// In function parameters (very common in React)
function renderCard({ title, subtitle = '', pinned = false }) { … }
renderCard({ title: 'Hello' });

Spread & copying

const copy   = { ...user };            // shallow copy
const updated = { ...user, age: 21 };  // copy + change (immutable update)
const merged  = { ...defaults, ...user }; // later keys win

const a = { x: 1 };
const b = a;                          // ⚠ SAME reference β€” not a copy!
b.x = 2;                              // a.x is also 2 now
const c = { ...a };                   // βœ… real (shallow) copy

// Deep copy (nested objects) β€” modern way
const deep = structuredClone(user);    // handles nested, dates, maps
const legacy = JSON.parse(JSON.stringify(user)); // functions/dates lost
⚠ Reference types: objects and arrays are compared by reference: {a:1} === {a:1} is false. This breaks includes on arrays of objects β€” match by a unique id instead: list.find(x => x.id === id).
JSON round-trip: JSON.stringify drops functions and undefined, converts Date to a string, and throws on circular references. For saving to localStorage or sending to an API that's exactly what you want β€” just remember it's a one-way trip for special types.

08 The DOM β€” Making the Page Interactive Intermediate

The DOM (Document Object Model) is the tree of objects the browser builds from your HTML. Every tag becomes an element node, every piece of text a text node. JavaScript reads and edits this tree, and the browser re-renders the page.

window vs document: document is the page's HTML tree; window is the whole browser context (scrollbar, timers, localStorage, global variables). window.document β†’ the same document.

Selecting elements

// Returns ONE element or null
document.querySelector('#save');           // any CSS selector
document.querySelector('.card.title');
document.querySelector('input[name="email"]');

// Returns a LIVE HTMLCollection (use [i] or [...list])
document.getElementsByClassName('btn');
document.getElementsByName('q');

// Returns a STATIC NodeList
document.querySelectorAll('.btn');        // forEach works directly βœ…

// Direct references
document.getElementById('header');
document.title;  document.body;  document.documentElement;  // <html>
πŸ’‘ Tip: querySelector accepts any CSS selector you already know. Give elements id for singletons and class/data-* attributes for groups β€” e.g. [data-theme="dark"], [hidden].

Changing content & attributes

el.textContent = 'Hello';          // safe β€” escapes HTML
el.innerHTML  = '<b>Hello</b>';   // ⚠ parses HTML β€” NEVER with user input (XSS)

el.value;                          // for <input> / <textarea> β€” read
el.value = 'typed text';          // set
el.checked = true;                // checkboxes / radios
el.disabled = true;
el.hidden = true;                // boolean attribute

el.setAttribute('href', '/docs');
el.getAttribute('href');
el.removeAttribute('disabled');
el.dataset.id   // data-id β†’ dataset.id (camelCase: data-user-id β†’ dataset.userId)
el.classList.add('active');
el.classList.remove('active');
el.classList.toggle('active');
el.classList.contains('active');  // true/false
el.style.color = 'crimson';       // inline styles β€” prefer CSS classes
⚠ XSS rule: if text comes from a user, an API or a URL query β€” put it in textContent, never innerHTML. innerHTML with attacker data executes their scripts. Use textContent for text; if you must inject markup, sanitize it first.

Creating, inserting, removing nodes

// Create
const li = document.createElement('li');
li.textContent = 'Task 3';
li.classList.add('todo');
li.dataset.id = '3';

// Insert
list.append(li);                    // end of list (multiple args ok)
list.prepend(li);                   // start
list.append(document.createElement('hr'));
li.after(el);  li.before(el);       // siblings
list.appendChild(li);               // old API β€” one node only
list.insertBefore(li, list.firstChild);

// Insert HTML string (trusted content only)
list.insertAdjacentHTML('beforeend', '<li>Hi</li>');

// Remove
li.remove();                        // modern
list.removeChild(li);               // old

Traversing the tree

el.parentElement
the containing element
el.children
element children only (live)
el.firstElementChild / lastElementChild
first/last child element
el.nextElementSibling
next node at same level
el.closest('.card')
walk UP β€” nearest matching ancestor
el.matches('.card')
does this element match the selector?
closest + event delegation: event.target.closest('.btn') finds which button was clicked even if you clicked the icon inside it β€” this powers the delegation pattern in the next section.

Reading layout & geometry

el.getBoundingClientRect();   // {x, y, top, left, right, bottom, width, height}
el.offsetWidth; el.offsetHeight;         // rendered size (rounded)
el.scrollLeft; el.scrollTop;             // scroll position
window.innerWidth; window.innerHeight;   // viewport size
window.scrollY;                          // how far the page is scrolled

getComputedStyle(el).color;             // final computed CSS value
✍ Try it

Build a to-do list: an <input>, a <ul> and a Save button. On Save, create an <li> with the text, append it, clear the input, and refocus it. Add a βœ• span in each <li> that removes it when clicked.

09 Events & Event Delegation Intermediate

An event is a signal that something happened: a click, a keypress, a page load. You attach a listener function and the browser calls it when the event fires.

Adding & removing listeners

button.addEventListener('click', (event) => {
  console.log('clicked!', event.target);   // event carries rich data
});

// Options object
button.addEventListener('click', handler, { once: true });  // auto-removes
button.addEventListener('scroll', handler, { passive: true }); // never blocks
button.addEventListener('keydown', handler, { capture: true }); // ancestor phase

// Remove β€” must be the SAME function reference
button.removeEventListener('click', handler);
⚠ Anonymous-function trap: addEventListener('click', () => work()) then later removeEventListener('click', () => work()) does nothing β€” they are different objects. Store the function in a variable and use it for both add and remove.

The event object

form.addEventListener('submit', (e) => {
  e.preventDefault();                      // stop page reload β€” essential
  const data = new FormData(e.target);
  console.log(data.get('email'));
});

input.addEventListener('input', (e) => {    // fires on every keystroke
  console.log(e.target.value);
});

input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') submit();
  if (e.key === 'Escape') close();
  if (e.ctrlKey && e.key === 's') { e.preventDefault(); save(); }
});
e.target
the element that ORIGINALLY fired the event
e.currentTarget
the element the listener is attached to
e.preventDefault()
cancel the default action (submit, link)
e.stopPropagation()
stop bubbling to parent listeners
e.key / e.code
"Enter" vs physical key position
e.timeStamp
ms since page load

Bubbling & delegation

Clicks on a child bubble up through its ancestors. Instead of one listener per item (slow with thousands of rows), attach one listener to the container:

// βœ… ONE listener for the whole list β€” works for items added later too
list.addEventListener('click', (e) => {
  const btn = e.target.closest('[data-action="delete"]');
  if (!btn) return;                       // clicked elsewhere
  btn.closest('li').remove();
});
πŸ’‘ Why delegate? (1) one listener instead of hundreds β€” less memory, (2) works for elements that don't exist yet (dynamically injected rows), (3) easy teardown β€” remove one listener.

Common events cheat sheet

click / dblclick
mouse buttons
input / change
typing (live) vs commit (blur/select)
submit
form β€” always preventDefault for AJAX
keydown / keyup
keyboard
mouseenter / mouseleave
hover (don't bubble)
focus / blur
element focus (don't bubble β€” use focusin/focusout)
DOMContentLoaded
HTML parsed; images/styles may still load
load
everything incl. images fully loaded
window.addEventListener('DOMContentLoaded', init);

// resize / scroll β€” throttle with rAF or a debounce
window.addEventListener('resize', debounce(() => relayout(), 150));
✍ Try it

Build an accordion FAQ with one delegated click listener on the container. Add a "collapse all" button that works even though the items were created after page load.

10 Async JavaScript, Promises & Fetch Advanced

JavaScript is single-threaded: one thing at a time. Network requests and timers must not freeze the page, so they run "in the background" and JS continues. When they finish, the event loop runs your callback. This model is called non-blocking I/O.

Callbacks β†’ Promises β†’ async/await (the evolution)

// 1. Callback (old style β€” "callback hell" with nesting)
getUser(id, (err, user) => {
  if (err) return handle(err);
  getPosts(user, (err, posts) => {
    // nested, hard to read, hard to error-handle
  });
});

// 2. Promise β€” a future value with .then chaining
getUser(id)
  .then(user => getPosts(user))
  .then(posts => render(posts))
  .catch(err => handle(err))     // single place for ALL errors
  .finally(() => hideSpinner());

// 3. async/await β€” looks synchronous, stays non-blocking (preferred)
try {
  const user  = await getUser(id);
  const posts = await getPosts(user);
  render(posts);
} catch (err) {
  handle(err);
} finally {
  hideSpinner();
}
Promise states: pending β†’ fulfilled (a value) or rejected (an error). A settled promise never changes again. await pauses that async function only β€” the rest of the page keeps running.

Creating promises

const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function retry(fn, tries = 3) {
  for (let i = 0; i < tries; i++) {
    try { return await fn(); }
    catch (e) { if (i === tries - 1) throw e; await wait(2 ** i * 500); }
  }
}

Parallel vs sequential

// ⚠ Sequential β€” waits for each (slow: 1+1 = 2s)
const u = await fetchUser();
const p = await fetchPosts();

// βœ… Parallel β€” start both together (1s total)
const [u, p] = await Promise.all([fetchUser(), fetchPosts()]);

// First to finish wins (with a timeout)
const fast = await Promise.race([fetchFromA(), fetchFromB()]);

// All settle regardless of rejection
const results = await Promise.allSettled([fetchUser(), fetchPosts()]);
results.forEach(r => r.status === 'fulfilled' ? r.value : r.reason);

// Run N tasks at a time (concurrency limit)
await Promise.all(urls.map(u => fetch(u)));     // all at once
⚠ Two classic bugs: (1) await outside an async function β†’ SyntaxError; (2) forgetting await β†’ you pass a Promise around instead of its value, and errors silently don't reach your catch.

fetch β€” talking to APIs

// GET
const res = await fetch('https://api.example.com/users?limit=10');
if (!res.ok) throw new Error(`HTTP ${res.status}`);   // 404/500 do NOT throw!
const users = await res.json();

// POST with JSON body
const created = await fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Laksh', role: 'admin' }),
}).then(r => { if (!r.ok) throw new Error('POST failed'); return r.json(); });

// DELETE / PUT
await fetch(`/api/users/${id}`, { method: 'DELETE' });

// Form data (uploads)
const form = new FormData(fileForm);
await fetch('/upload', { method: 'POST', body: form });  // boundary auto-set
⚠ fetch only rejects on network failure (offline, DNS). A 404 or 500 resolves normally with res.ok === false β€” you must check it yourself or errors vanish.

AbortController & timeouts

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);          // 5s timeout

try {
  const res = await fetch('/api/slow', { signal: ctrl.signal });
} catch (err) {
  if (err.name === 'AbortError') console.log('timed out or cancelled');
}

// Cancel a stale request when the user types again
let current;
async function search(q) {
  current?.abort();
  current = new AbortController();
  const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`,
                         { signal: current.signal });
  return res.json();
}
πŸ’‘ Tip: combine AbortController with a debounce (fire only after typing pauses) for search-as-you-type boxes. Without both, you'll render results from an old request over newer ones.

11 Errors & Debugging Intermediate

try / catch / finally & throwing

try {
  const data = JSON.parse(raw);
  if (!data.id) throw new Error('Missing id');   // your own errors
} catch (err) {
  console.error(err.message);
} finally {
  hideSpinner();            // ALWAYS runs β€” cleanup code
}

// Custom error class β€” better stack traces & typed handling
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

throw new ValidationError('Email is invalid', 'email');
Error
generic β€” message + stack
TypeError
wrong type (null.foo, not a function)
ReferenceError
variable doesn't exist (or TDZ)
SyntaxError
invalid code β€” parse time, can't be caught at runtime
RangeError
value out of range (recursion too deep)
URIError
bad encodeURIComponent input

Error properties

try { risky(); } catch (err) {
  err.message        // human-readable text
  err.name           // "TypeError"
  err.stack          // file:line:col chain β€” the debugging gold mine
}

Debugging techniques

console.log('value:', value);      // basics still work
console.table(rows);              // arrays of objects β†’ readable table
debugger;                         // programmatic breakpoint (DevTools open)
// debugger; β†’ remove or guard before shipping

// Source of a bug: log with context, not alone
console.assert(user.id, 'user missing id', user);
Step over (F10)
execute next line, don't enter functions
Step into (F11)
descend into the called function
Step out (Shift+F11)
finish current function, return to caller
Watch / Scope pane
live values of local + closure variables
Call stack
who called this β€” click frames to jump
Blackboxing
ignore library frames while stepping
Unhandled rejections: an await with no try/catch (and no .catch()) crashes to the console as an unhandled promise rejection. In Node, that can exit the process. Catch at the boundary where you can inform the user.

12 Modules β€” Splitting Code Across Files Advanced

Modules let you split a program into files with explicit imports/exports. Each module has its own scope β€” no accidental globals, no load-order puzzles.

Exporting

// utils.js
export const PI = 3.14159;                       // named export (preferred)
export function clamp(n, min, max) {
  return Math.min(Math.max(n, min), max);
}
export default function greet(name) { return `Hi ${name}`; }  // one per file

// Or group at the end
function a() {} function b() {}
export { a, b, a as helperA };

// Re-export (barrel files)
export * from './math.js';
export { clamp } from './utils.js';

Importing

import greet, { clamp, PI } from './utils.js';   // default + named
import { clamp as limit } from './utils.js';      // rename
import * as utils from './utils.js';               // namespace
import './side-effect.css';                       // side effects only

// Dynamic import β€” load on demand (code-splitting)
const { renderChart } = await import('./chart.js');

Using modules in HTML

<script type="module" src="app.js"></script>
<!-- type="module" implies defer, runs in strict mode,
     and URLs need explicit ./ or full paths -->
Module rules: (1) strict mode by default (no undeclared variables), (2) exports are live views β€” values update for importers, (3) imports are hoisted and loaded once, (4) you cannot import inside an if (use dynamic import()), (5) each module = one HTTP request β€” bundlers (Vite/webpack) merge them for production.
πŸ’‘ Tip: structure code by feature, not by type β€” features/auth/{api.js, validators.js, LoginPage.js} keeps related code together. Export small, focused functions with clear names; a module that exports 40 things is doing too much.

13 Modern JS β€” ES6+ Features You'll Use Daily Intermediate

Template literals & tagged templates

const name = 'Laksh', points = 42;
const msg = `Hello ${name}! You scored ${points}.
             Runs across lines too.`;

// String methods
msg.startsWith('Hello');  msg.endsWith('too.');  msg.includes('scored');
'abc'.repeat(3);          // "abcabcabc"
'  hi  '.trim();          // "hi"
'-'.padStart(8, '-');      // "-------"

Optional chaining & nullish coalescing

const city = user?.address?.city ?? 'Unknown';
//          ^^^^^ chain: undefined anywhere β†’ undefined (no crash)
//                                    ?? only falls back on null/undefined

user?.save?.();                 // call only if save exists
arr?.[0]                        // first element if arr exists
data?.name?.[0]

// ⚠ Difference from ||
0 ?? 'fallback'   // 0        β€” 0 is NOT nullish
0 || 'fallback'   // "fallback" β€” 0 is falsy
// Use ?? when 0 and '' are VALID values (counts, search boxes)

Symbols, iterators & generators (conceptual)

// Generators β€” functions that can pause/resume (yield)
function* range(start, end) {
  for (let i = start; i < end; i++) yield i;
}
[...range(0, 5)]      // [0,1,2,3,4] β€” no giant array in memory

// Iterables: anything with Symbol.iterator (arrays, strings, Map, Set)
for (const ch of 'hi') console.log(ch);   // "h", "i"

Map & Set vs plain objects

// Map β€” any key type, remembers insertion order
const cache = new Map();
cache.set(userId, userData);        // keys are NOT stringified
cache.get(userId);
cache.has(userId);  cache.delete(userId);
cache.size;
for (const [key, val] of cache) { … }

// Set β€” unique values only
const unique = new Set([1, 2, 2, 3]);   // {1,2,3}
unique.add(4);  unique.has(2);  unique.delete(1);
const arr = [...new Set(ids)];      // dedupe an array β€” classic trick
When to use what: plain object for plain data (JSON-friendly, cheap); Map for dynamic keys, frequent add/remove, or when you need insertion order guaranteed; Set for membership tests and dedupe (O(1) lookups).

Class syntax (syntactic sugar over prototypes)

class User {
  static count = 0;                     // shared by the class itself

  constructor(name, role = 'user') {
    this.name = name;
    this.role = role;
    User.count++;
  }

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

  get initials() { return this.name[0]; }   // read like a property
}

class Admin extends User {
  constructor(name) { super(name, 'admin'); }  // must call super() first
  ban(user) { … }
}

const u = new User('Laksh');
u.greet();  u.initials;  User.count;
πŸ’‘ Modern additions: Object.groupBy(arr, fn) (ES2024), Array.prototype.toSorted/toReversed/toSpliced (non-mutating), Promise.withResolvers() (ES2024), and using declarations for automatic resource cleanup. Check MDN for browser support before using very new features.

14 Web Storage & State on the Client Beginner

// localStorage β€” persists forever, strings only, ~5 MB per origin
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme');      // "dark" or null
localStorage.removeItem('theme');
localStorage.clear();

// Objects must be serialized
localStorage.setItem('user', JSON.stringify(user));
const u = JSON.parse(localStorage.getItem('user') ?? '{}');

// sessionStorage β€” same API, cleared when the tab closes
sessionStorage.setItem('step', '2');
⚠ Three rules: (1) storage only holds strings β€” everything else needs JSON.stringify/parse; (2) never store passwords/tokens meant to be secret β€” anything JS can read, scripts can read (an XSS bug exposes it); (3) getItem returns null when missing, and JSON.parse(null) doesn't throw β€” but JSON.parse('garbage') does. Wrap parses in try/catch.
// Safe helpers
function loadJSON(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) : fallback;
  } catch {
    return fallback;              // corrupted entry
  }
}
function saveJSON(key, value) {
  try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* quota */ }
}
localStorage
persists across sessions β€” theme, prefs, drafts
sessionStorage
per tab, dies with the tab β€” wizard steps
Cookies
sent with HTTP requests β€” auth sessions (httpOnly)
IndexedDB
large structured data, offline apps (via libraries)
✍ Try it

Build a light/dark toggle that saves the choice with localStorage and applies it on page load before first paint (inline script in <head> to avoid a flash β€” exactly what this guide's own theme script does).

15 Timers, Browser APIs & Performance Intermediate

setTimeout & setInterval

const timerId = setTimeout(() => showPopup(), 3000);   // once, after 3s
clearTimeout(timerId);

const pollId = setInterval(pollStatus, 5000);           // every 5s
clearInterval(pollId);                                    // stop!

// Chaining instead of setInterval (guarantees no overlap)
function poll() {
  check().finally(() => { timeoutId = setTimeout(poll, 5000); });
}
⚠ Debounce vs throttle: debounce β€” run only after calls stop (search box: wait 300 ms after the last keystroke). throttle β€” run at most once per window (scroll handler: once every 100 ms). Without them, resize/scroll handlers fire hundreds of times per second.
// Debounce
function debounce(fn, ms = 300) {
  let t;
  return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}

// Throttle (leading edge)
function throttle(fn, ms = 100) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= ms) { last = now; fn(...args); }
  };
}

// requestAnimationFrame β€” schedule work before the next repaint (60fps)
function onScroll() {
  requestAnimationFrame(() => { parallaxY = window.scrollY * 0.5; });
}

Useful browser APIs

navigator.clipboard
writeText() / readText() β€” needs HTTPS
Geolocation
navigator.geolocation.getCurrentPosition()
IntersectionObserver
lazy-load images, infinite scroll, reveal-on-scroll
ResizeObserver
react to element size changes
MutationObserver
watch DOM changes (browser extensions)
matchMedia
respond to prefers-reduced-motion, breakpoints
History API
pushState/replaceState β€” SPA routing
Web Workers
heavy compute off the main thread
// Copy to clipboard (secure context required)
await navigator.clipboard.writeText(text);

// Lazy-load with IntersectionObserver
const io = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (e.isIntersecting) { e.target.src = e.target.dataset.src; io.unobserve(e.target); }
  });
}, { rootMargin: '200px' });
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));

// Respect user preferences
if (matchMedia('(prefers-reduced-motion: reduce)').matches) disableAnimations();
Performance instincts: (1) never update the DOM inside a raw scroll/resize handler β€” debounce or rAF; (2) cache repeated querySelector results; (3) build lists with one innerHTML write instead of thousands of append calls (or use DocumentFragment); (4) measure first: DevTools Performance tab shows real jank.

16 Best Practices & Common Pitfalls Advanced

Style & correctness

const by default
let only when reassigning; never var
=== not ==
strict comparison always
camelCase
variables/functions, kebab-case for CSS/HTML
Early return
handle errors first β€” avoid deep nesting
Pure helpers
small functions that don't hide side effects
Comments why, not what
code says what; comment explains intent
// ❌ Deep nesting
function process(order) {
  if (order) {
    if (order.paid) {
      if (order.items.length) { … }
    }
  }
}

// βœ… Guard clauses β€” flatten
function process(order) {
  if (!order) return;
  if (!order.paid) return;
  if (!order.items.length) return;
  …
}

The 10 classic pitfalls

  1. Missing === β€” if (value = 10) assigns instead of compares β†’ infinite loop
  2. Forgetting e.preventDefault() on form submit β†’ page reloads, state lost
  3. this lost in callbacks β€” arrow functions or bind when passing methods
  4. Async mistakes β€” missing await, or await in a loop when calls are independent
  5. fetch without res.ok check β€” 500s look like success
  6. innerHTML with user data β€” XSS vulnerability
  7. Mutating shared arrays/objects β€” UI doesn't update or state corrupts
  8. Stale closures in loops β€” var i captured by all callbacks; use let
  9. Memory leaks β€” listeners never removed, timers never cleared, detached DOM retained by closures
  10. Using == null only β€” otherwise strict; parseInt without radix (pass 10)
// Stale closure trap β€” all callbacks print 3, 3, 3 (with var)
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);

// Fixed β€” let creates a new binding each iteration
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);   // 0 1 2

Tooling every serious project uses

ESLint
static bug-catching + style rules
Prettier
automatic consistent formatting
TypeScript / JSDoc
catch type bugs before runtime
Vitest / Jest
unit tests for logic (pure functions first)
Vite
dev server + bundler β€” instant HMR
git hooks (husky)
lint+test before every commit
// npm scripts worth having
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "lint": "eslint src --max-warnings 0",
  "test": "vitest run",
  "format": "prettier --write ."
}
πŸ’‘ Debugging mindset: reproduce β†’ isolate (comment out half) β†’ read the error carefully (first stack frame is usually yours) β†’ check assumptions (log the actual value) β†’ fix β†’ add a test so it can't come back.

17 Practice Projects Build to Learn

Work up the ladder β€” each project reuses concepts from earlier sections. Build them in vanilla JS first; frameworks later.

1. Counter App
DOM, events, state in a variable Β· Sections 8, 9
2. Dark/Light Toggle
classList, localStorage persistence Β· Sections 8, 14
3. To-Do List
arrays, delegated events, CRUD Β· Sections 6, 9
4. Quiz App
objects, conditionals, score logic Β· Sections 4, 7
5. Weather App
fetch, async/await, error states Β· Section 10
6. Expense Tracker
map/reduce, forms, storage, charts Β· Sections 6, 14

Starter skeleton β€” anything interactive

// app.js
'use strict';

const state = { items: [], filter: 'all' };

function init() {
  loadState();
  bindEvents();
  render();
}

function bindEvents() {
  document.querySelector('#add').addEventListener('click', addItem);
  document.querySelector('#list').addEventListener('click', onListClick);  // delegated
}

function render() {
  const list = document.querySelector('#list');
  list.innerHTML = state.items.map(itemHTML).join('');   // one write
}

document.addEventListener('DOMContentLoaded', init);
The pattern above scales: hold data in a state object β†’ change state β†’ call render() to redraw from state. This unidirectional flow is exactly what React/Vue do under the hood β€” you're already "thinking in frameworks".

Where to go next

TypeScript
JavaScript with static types β€” the industry standard for large apps
React / Vue
component frameworks built on these fundamentals
Node.js / Express
servers, REST APIs, databases
Test-driven dev
Vitest/Jest β€” confidence to refactor
MDN Web Docs
the definitive reference β€” developer.mozilla.org
You Don't Know JS
book series for deep understanding (free on GitHub)
πŸ’‘ Final advice: build something you want to use, break it deliberately, fix it, and read other people's code on GitHub. Fluency comes from shipping small things often β€” not from watching one more tutorial.