Home  ›  Web Development  ›  JavaScript

WEB DEVELOPMENT / JAVASCRIPT

JavaScript: Complete Guide to Modern JavaScript, DOM, APIs & Async Programming

Learn JavaScript from the fundamentals to DOM manipulation, events, functions, asynchronous programming, APIs, modules, browser storage and modern frontend development.

Every concept comes with a short, copyable example, and the tricky parts — closures, this, the event loop, promises — get proper explanations rather than one-liners.

EXAMPLE: CODE, PAGE AND CONSOLE
app.js
const button = document.querySelector(".button");

button.addEventListener("click", () => {
  console.log("Button clicked");
});
Page
Click me
> Button clicked
JavaScript code editor with browser preview showing an interactive web interface

THE BASICS

What Is JavaScript?

JavaScript is a programming language used to add behaviour, logic and interaction to websites and web applications. Where HTML describes structure and CSS describes presentation, JavaScript is what makes a page do things: respond to clicks and typing, update content without a reload, validate forms, fetch data from APIs, manage application state and drive complex interfaces.

Unlike HTML and CSS, JavaScript is a full programming language with variables, functions, conditionals, loops, data structures, objects and error handling. It’s also grown well beyond the browser — the same language powers server applications, build tooling, automation scripts, APIs, and desktop and mobile apps through surrounding frameworks and runtimes.

Calling it “just a scripting language” undersells it. JavaScript today runs large production applications, though its origins as a browser language still show in a few quirks this guide covers honestly rather than glossing over.

JavaScript can

THE FOUNDATION

HTML vs CSS vs JavaScript

HTML — Structure

Defines what content is and what it means, giving JavaScript the elements it selects and modifies. See our HTML guide.

CSS — Presentation

Controls appearance and layout, including the state-based styling JavaScript triggers by toggling classes. See our CSS guide.

JavaScript — Behaviour

Handles logic, interaction, data and application state, usually by changing the document or the classes applied to it.

A healthy division of labour looks like this: HTML carries the semantics, CSS owns every visual decision, and JavaScript changes state rather than setting styles directly. That keeps styling in stylesheets where it can be maintained and overridden. See our web development guides for how the whole stack fits together.

HOW IT RUNS

How JavaScript Works in the Browser

When a browser parses HTML and meets a script, it hands the code to a JavaScript engine — V8 in Chromium browsers, SpiderMonkey in Firefox, JavaScriptCore in Safari. Engines differ in implementation while aiming to implement the same language specification, so behaviour is broadly consistent but not guaranteed identical, especially for very new features.

A JavaScript engine is not the same thing as a browser engine. The browser engine handles parsing, layout and rendering; the JavaScript engine executes code. The engine alone knows nothing about web pages — the DOM, fetch, localStorage, timers and events are all Web APIs provided by the browser as the host environment.

That distinction matters. ECMAScript, standardised as ECMA-262 and developed through the TC39 committee, defines the language itself: syntax, types, operators, functions, objects, promises. Everything else your browser code touches is host-provided. It’s why fetch and localStorage aren’t part of the language specification, and why the same JavaScript won’t necessarily run in a different runtime. JavaScript and ECMAScript aren’t different languages — one is the common name, the other the specification.

HTML page loads
Browser hands script to the JavaScript engine
Engine executes the code
Code calls Web APIs — DOM, fetch, storage, timers
Document and interface update
Interactive web page
How JavaScript runs in a browser from page load through the engine and Web APIs to an updated page
 JavaScript — syntax, comments and variablesEXAMPLE
// Single-line comment

/*
  Multi-line comment
*/

const name = "Alex";          // cannot be reassigned
let count = 0;                // can be reassigned
count = count + 1;

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

// const prevents reassignment, not mutation
const user = { name: "Alex" };
user.name = "Sam";            // allowed
// user = {};                 // TypeError

SYNTAX

Syntax, Comments and Variables

JavaScript code is made of statements that perform actions and expressions that produce values, built from identifiers, values, operators and blocks in braces. Comments use // for a single line and /* */ for several — useful for explaining non-obvious decisions, but never for secrets, since page source is always readable.

Three keywords declare variables. const creates a block-scoped binding that can’t be reassigned; let creates one that can; var is the original form, function-scoped with older hoisting behaviour. The common advice is const by default and let when reassignment is genuinely needed. Rather than treating var as forbidden, understand why its scoping surprises people — you’ll meet it in existing code.

One distinction trips up almost everyone: const prevents reassignment, not mutation. A const object can have its properties changed; what you can’t do is point the name at something else.

DATA TYPES

JavaScript Data Types

JavaScript has seven primitive types plus objects. Primitives hold a single value and are copied by value; objects are reference types, which is why assigning one to a second variable gives two names for the same thing.

string

Text, in single quotes, double quotes or backticks.

number

Integers and decimals in one floating-point type.

bigint

Integers beyond the safe range of number.

boolean

true or false.

undefined

A variable declared but not assigned.

null

An intentional absence of a value.

symbol

Unique values, often used as special object keys.

object

Everything else — objects, arrays, functions, dates.

Arrays and functions are objects in the type system, though you use them very differently in practice — typeof [] returns "object", and Array.isArray() is the reliable way to check.

null versus undefined: undefined usually means a value was never assigned — an uninitialised variable, a missing argument, a property that doesn’t exist. null is an explicit “deliberately empty” that you assign yourself. In practice teams use them differently, and API responses may use either.

typeof reports a value’s type, with one famous exception: typeof null returns "object". That’s a longstanding behaviour kept for backwards compatibility, not a statement that null is an object.

Type conversion can be explicit with String(), Number() and Boolean(), or implicit when operators coerce values for you — the reason "5" + 1 gives "51" while "5" - 1 gives 4. Explicit conversion is easier to reason about.

 JavaScript — types, typeof and truthinessEXAMPLE
typeof "Hello";     // "string"
typeof 42;          // "number"
typeof true;        // "boolean"
typeof undefined;   // "undefined"
typeof null;        // "object"  (historical behaviour)
typeof [];          // "object"
Array.isArray([]);  // true

// Explicit conversion
Number("42");       // 42
String(42);         // "42"
Boolean("");        // false

// Falsy values
// false, 0, -0, 0n, "", null, undefined, NaN
// Everything else is truthy — including [] and {}

Truthy and falsy matter whenever a value is used in a condition. The falsy values are false, 0, -0, 0n, the empty string, null, undefined and NaN. Everything else is truthy — notably empty arrays and empty objects, which look empty but pass an if check.

VALUES

Strings, Numbers and BigInt

Strings can use single or double quotes, or backticks for template literals, which support interpolation with ${}, multi-line text and embedded expressions. Template literals are usually the clearest option. Useful string members include length, includes(), startsWith(), endsWith(), slice(), trim(), replace(), toUpperCase() and toLowerCase(). Strings are immutable — these methods return new strings.

Numbers are a single type covering integers and decimals, stored as floating-point values. That’s why 0.1 + 0.2 doesn’t give exactly 0.3 — a property of binary floating-point arithmetic, not a JavaScript bug. For money, work in the smallest unit such as paise or cents, or use a decimal library. Watch for NaN (the result of invalid arithmetic, and not equal to itself) and Infinity.

BigInt handles integers beyond the safe range for number, written with an n suffix. It doesn’t mix with number in ordinary arithmetic — you must convert explicitly. Useful for large IDs and precise integer maths; unnecessary for everyday values.

 JavaScript — strings, numbers and BigIntEXAMPLE
const name = "Alex";
const greeting = `Hello, ${name}!`;

name.length;              // 4
name.toUpperCase();       // "ALEX"
"  spaced  ".trim();      // "spaced"
"webdesign".includes("web"); // true

// Floating-point precision
0.1 + 0.2;                // 0.30000000000000004
(0.1 + 0.2).toFixed(2);   // "0.30"

Number.isNaN(Number("abc")); // true

// BigInt
const big = 9007199254740993n;
// big + 1;               // TypeError — cannot mix types
big + 1n;                 // works
 JavaScript — operators and modern syntaxEXAMPLE
// Arithmetic
5 + 2;  5 - 2;  5 * 2;  5 / 2;  5 % 2;  5 ** 2;

// Comparison
1 === "1";   // false — strict, no coercion
1 ==  "1";   // true  — loose, coerces types
null == undefined;  // true
null === undefined; // false

// Logical and ternary
const status = isActive && hasAccess ? "open" : "closed";

// Optional chaining — safe nested access
const city = user?.address?.city;
user.save?.();          // only calls if it exists

// Nullish coalescing vs OR
const count1 = 0 ?? 10;  // 0   — only null/undefined fall through
const count2 = 0 || 10;  // 10  — any falsy value falls through

OPERATORS

Operators, Equality and Modern Syntax

JavaScript has the usual arithmetic operators plus % for remainder and ** for exponentiation, assignment operators including compound forms, comparison operators, logical &&, || and !, and the ternary conditional.

=== versus == is the classic question. Strict equality compares without type coercion; loose equality applies conversion rules before comparing. Default to === because it’s predictable. That doesn’t make == always wrong — value == null is a common, deliberate way to check for null or undefined in one go.

Optional chaining (?.) stops evaluation and returns undefined if a link in the chain is null or undefined, instead of throwing. Invaluable with API data whose shape you don’t control.

Nullish coalescing (??) supplies a fallback only when the left side is null or undefined. That’s the crucial difference from ||, which also replaces 0, empty strings and false — a frequent source of bugs where a legitimate zero silently becomes a default.

CONTROL FLOW

Conditions, Loops and Array Methods

Conditionals use if, else if and else, with switch for comparing one value against several cases — remember break, or execution falls through to the next case, and include a default. The ternary operator handles short either-or expressions.

Loops include the classic for, while, do...while, and two for-in-style forms worth keeping straight: for...of iterates over the values of an iterable such as an array, while for...in iterates over the keys of an object and is not intended for arrays.

In practice, array methods replace most manual loops and read better. map() transforms each item into a new array, filter() keeps items matching a test, reduce() folds an array into a single value, find() and findIndex() locate the first match, some() and every() test conditions, includes() checks membership, and forEach() runs a function per item without returning anything. Most return new arrays rather than mutating the original, which makes them easier to reason about.

 JavaScript — conditions, loops and array methodsEXAMPLE
if (score >= 90) {
  grade = "A";
} else if (score >= 80) {
  grade = "B";
} else {
  grade = "C";
}

switch (role) {
  case "admin":  label = "Admin";  break;
  case "editor": label = "Editor"; break;
  default:       label = "User";
}

const prices = [120, 80, 250, 45];

for (const price of prices) { /* values */ }

const withTax   = prices.map(p => p * 1.18);
const expensive = prices.filter(p => p > 100);
const total     = prices.reduce((sum, p) => sum + p, 0);
const firstBig  = prices.find(p => p > 100);
const anyCheap  = prices.some(p => p < 50);
const allValid  = prices.every(p => p > 0);
 JavaScript — functionsEXAMPLE
// Declaration — hoisted, available before its definition
function add(a, b) {
  return a + b;
}

// Expression
const multiply = function (a, b) {
  return a * b;
};

// Arrow function — implicit return, lexical this
const square = n => n * n;

// Default and rest parameters
function greet(name = "there", ...titles) {
  return `Hello, ${titles.join(" ")} ${name}`;
}

// Callback: a function passed to another function
[1, 2, 3].forEach(number => console.log(number));

// Higher-order function: returns a function
function withPrefix(prefix) {
  return message => `${prefix}: ${message}`;
}
const warn = withPrefix("Warning");

FUNCTIONS

Functions, Arrow Functions and Callbacks

Functions take parameters, receive arguments and optionally return a value. A function declaration is hoisted, so it can be called before it appears; a function expression assigned to a variable is not.

Arrow functions are more than shorter syntax. A single expression body returns implicitly, but the important difference is that arrows have no this of their own — they inherit it from the surrounding scope. That makes them excellent as callbacks inside methods and unsuitable as object methods or constructors where you need a dynamic this.

A callback is simply a function passed to another function to be invoked later — the basis of array methods, event listeners and older asynchronous APIs. A higher-order function is one that takes or returns a function; map(), filter() and reduce() are the ones you’ll use daily.

SCOPE & CONTEXT

Scope, Hoisting, Closures and this

Scope determines where a name is visible. Global scope is accessible everywhere, function scope is limited to a function, and block scope — which let and const respect and var does not — is limited to the nearest braces. JavaScript uses lexical scope: where a function is written decides what it can see, not where it’s called.

Hoisting is often mis-taught as “code moves to the top”. What actually happens is that declarations are registered before execution. var declarations are initialised as undefined, so reading one early gives undefined rather than an error. let and const are registered but uninitialised, sitting in a temporal dead zone until their declaration runs — reading them early throws. Function declarations are fully available.

A closure is a function that keeps access to the variables of the scope where it was created, even after that scope has finished. It’s the mechanism behind private state, function factories and most callback patterns.

this depends on how a function is called, not where it’s defined. In a method call it’s the object before the dot; in a standalone regular function it’s undefined in strict mode or the global object otherwise; with new it’s the new instance; and call(), apply() and bind() set it explicitly. Arrow functions are the exception — they take this from their enclosing scope, which is exactly why they work well as callbacks inside methods.

 JavaScript — closures and thisEXAMPLE
function createCounter() {
  let count = 0;                 // stays alive via closure

  return function () {
    count += 1;
    return count;
  };
}

const next = createCounter();
next();  // 1
next();  // 2

// this depends on how a function is called
const timer = {
  seconds: 0,

  startBroken() {
    setInterval(function () {
      this.seconds += 1;         // wrong this
    }, 1000);
  },

  startWorking() {
    setInterval(() => {
      this.seconds += 1;         // arrow inherits this
    }, 1000);
  }
};

// Explicit binding
const bound = timer.startWorking.bind(timer);
 JavaScript — objects, arrays and destructuringEXAMPLE
const user = {
  name: "Alex",
  age: 30,
  address: { city: "Pune" },
  greet() {
    return `Hello, ${this.name}`;
  }
};

user.name;            // dot access
user["age"];          // bracket access

// Destructuring with a default
const { name, age, role = "member" } = user;
const { address: { city } } = user;

const colors = ["red", "blue", "green"];
const [first, , third] = colors;

// Spread — copy and extend
const updated = { ...user, role: "admin" };
const combined = [...colors, "yellow"];

// Array methods that mutate
colors.push("pink");    colors.pop();
colors.unshift("cyan"); colors.shift();
colors.slice(0, 2);     // returns a copy

// Map and Set
const settings = new Map([["theme", "dark"]]);
settings.get("theme");
const unique = new Set([1, 2, 2, 3]);  // {1, 2, 3}

DATA STRUCTURES

Objects, Arrays, Destructuring and Collections

Objects group related data and behaviour as key-value pairs, accessed with dot or bracket notation. Bracket notation is needed for dynamic keys and names that aren’t valid identifiers. Functions stored as properties are methods.

Destructuring pulls values out of objects and arrays into variables, with defaults for missing values and renaming where needed. It’s everywhere in modern code — function parameters, imports, API responses.

Spread (...) expands an object or array into another, which is the standard way to copy with changes rather than mutating in place. Note that it’s a shallow copy — nested objects are still shared references. Rest parameters use the same syntax in the opposite direction, collecting remaining arguments into an array.

Arrays are ordered, zero-indexed lists. push, pop, shift, unshift and splice modify the original; slice, concat and map return new arrays. Knowing which is which prevents a lot of confusing bugs.

Map stores key-value pairs with keys of any type and preserved insertion order, unlike plain objects. Set stores unique values, making deduplication a one-liner.

BUILT-INS

Dates, Regular Expressions and JSON

Dates use the built-in Date object for creating, reading, comparing and formatting values. Date handling gets complicated quickly once time zones, daylight saving and locale formatting are involved. Intl.DateTimeFormat handles locale-aware formatting well; some projects reach for a date library, though that’s a judgement call rather than a requirement.

Regular expressions describe text patterns for matching, validating, searching and replacing. They’re powerful and easy to overuse — a complex regex is often harder to maintain than a few plain string checks, and email validation in particular is better left to the browser’s type="email" plus server-side verification.

JSON is a text format for exchanging data. JSON.stringify() converts a JavaScript value to a JSON string; JSON.parse() converts it back. They aren’t the same thing: JSON is text with stricter rules — double-quoted keys, no functions, no comments, no trailing commas. Parsing untrusted input can throw, so wrap it in try/catch.

 JavaScript — dates, regex and JSONEXAMPLE
const now = new Date();
now.getFullYear();
const later = new Date("2026-01-15");
later > now;              // dates compare directly

new Intl.DateTimeFormat("en-IN").format(now);

// Regular expressions
const hasDigits = /\d+/.test("abc123");      // true
"2026-01-15".replace(/-/g, "/");             // "2026/01/15"

// JSON
const json = JSON.stringify({ name: "Alex" });
// '{"name":"Alex"}'

try {
  const user = JSON.parse(json);
} catch (error) {
  console.error("Invalid JSON", error);
}

THE DOM

The DOM: Selecting and Changing the Page

The DOM (Document Object Model) is the browser’s live, structured representation of the document. JavaScript reads and modifies that tree, and the browser re-renders. The DOM is not part of the JavaScript language — it’s a Web API the browser provides — and it doesn’t stay identical to your original HTML once scripts start changing it.

Selecting: getElementById() fetches one element by id; querySelector() returns the first match for a CSS selector; querySelectorAll() returns all matches as a static NodeList you can iterate with forEach.

Changing content: prefer textContent, which sets plain text. innerHTML parses its input as markup, so inserting untrusted content that way is the classic route to cross-site scripting. Use innerHTML only with content you fully control.

Changing appearance: setting element.style directly works but scatters styling through your scripts. Toggling classes with classList — add(), remove(), toggle(), contains(), replace() — keeps the visual decisions in your CSS where they belong.

Creating elements with createElement(), setting textContent and appending is the safe way to build markup from data. When adding many nodes, build them in a fragment and append once rather than in a loop.

ILLUSTRATIVE DOM TREE
document
└── html
    └── body
        ├── header
        ├── main
        │   ├── h1
        │   ├── p
        │   └── button ← selected
        └── footer
Illustrative DOM tree showing HTML elements connected to JavaScript
 JavaScript — DOM selection and manipulationEXAMPLE
const heading = document.querySelector("h1");
const items   = document.querySelectorAll(".item");
const menu    = document.getElementById("menu");

// Safe text update
heading.textContent = "Updated heading";

// Class-based state — preferred over inline styles
menu.classList.toggle("is-open");
menu.classList.add("is-visible");
menu.classList.contains("is-open");   // true / false

// Creating elements safely
const item = document.createElement("li");
item.textContent = "JavaScript";
item.classList.add("item");
list.append(item);

items.forEach(el => el.classList.remove("active"));
EXAMPLE EVENT FLOW
User clicks a button
Browser creates an event object
Event listener runs your function
DOM or application state updates
Updated interface
PROPAGATION
Capture phase: document → parent → target
Target: the clicked element
Bubble phase: target → parent → document
Illustrative event flow showing a click travelling through capture, target and bubble phases

EVENTS

Events, Listeners and Delegation

Events are how JavaScript responds to what happens in the browser: click, input, change, submit, keydown, keyup, mouse events, focus, blur and load, among many others.

addEventListener() is the standard way to respond. It’s preferred over inline onclick attributes because it keeps behaviour out of markup, allows several listeners on one element, and can be removed later with removeEventListener().

Handlers receive an event object. event.target is the element that actually triggered it, event.currentTarget the element the listener is attached to — they differ once propagation is involved. event.preventDefault() stops the browser’s default action, such as a form submitting or a link navigating.

Events propagate. After reaching the target they bubble up through ancestors, and before that they travel down in the capture phase (opt in by passing { capture: true }). Bubbling is what makes event delegation possible: attach one listener to a parent and check event.target, instead of adding listeners to every child. That’s fewer listeners, and it works for elements added later.

 JavaScript — events, delegation and formsEXAMPLE
const button = document.querySelector(".button");

button.addEventListener("click", event => {
  event.preventDefault();
  button.textContent = "Clicked";
});

// Delegation — one listener for many children
list.addEventListener("click", event => {
  const remove = event.target.closest("button.remove");
  if (!remove) return;
  remove.closest("li").remove();
});

// Form handling with validation
form.addEventListener("submit", event => {
  event.preventDefault();

  const email = emailInput.value.trim();

  if (!email) {
    errorEl.textContent = "Enter your email address";
    emailInput.setAttribute("aria-invalid", "true");
    emailInput.focus();
    return;
  }

  errorEl.textContent = "";
  emailInput.removeAttribute("aria-invalid");
  // submit the data
});

Forms are the most common place events and validation meet. Listen for submit on the form rather than click on the button, so keyboard submission works. Call preventDefault() before handling the data yourself, read values from the inputs, and when validation fails, show a specific message, mark the field with aria-invalid and move focus to it. Client-side validation improves the experience but is never a security measure — anyone can bypass it, so always validate on the server too.

ASYNCHRONOUS JAVASCRIPT

Async JavaScript, Promises and async/await

Some work takes time: network requests, timers, file reads. Asynchronous JavaScript lets you start that work and carry on, handling the result when it arrives, instead of freezing everything until it finishes.

It’s worth being precise about what this does not mean. Browser JavaScript runs your code on a single main thread — asynchronous code isn’t running in parallel with it. The browser performs the waiting elsewhere and queues your callback to run when the main thread is free.

A Promise represents a value that isn’t available yet. It’s pending until it either fulfils with a value or rejects with a reason. .then() handles success, .catch() handles failure, and .finally() runs either way. Promises chain: each .then() returns a new promise, so returning a value passes it along and returning a promise waits for it. Promises don’t make code synchronous — they give asynchronous results a predictable shape.

async/await is syntax over the same mechanism. An async function always returns a promise; await pauses that function until a promise settles, without blocking the page, and lets you use ordinary try/catch. It creates no threads — it’s the same single-threaded model, written to read top to bottom.

ILLUSTRATIVE ASYNC FLOW
JavaScript starts a request
Execution continues — nothing blocks
Browser handles the network work
Promise settles: fulfilled or rejected
await resumes, or .then() runs
Interface updates
Illustrative asynchronous JavaScript workflow showing a request and Promise resolution
PROMISE LIFECYCLE
Pending
↓
Fulfilled
.then(value)
Rejected
.catch(error)
↓
.finally() — runs either way
Promise lifecycle from pending through fulfilled or rejected to finally
 JavaScript — promises and async/awaitEXAMPLE
// Promise chain
fetch("/api/products")
  .then(response => {
    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }
    return response.json();
  })
  .then(data => renderProducts(data))
  .catch(error => showError(error))
  .finally(() => hideSpinner());

// The same logic with async/await
async function loadProducts() {
  try {
    const response = await fetch("/api/products");

    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }

    const data = await response.json();
    renderProducts(data);
  } catch (error) {
    showError(error);
  } finally {
    hideSpinner();
  }
}

// Independent requests in parallel
const [users, orders] = await Promise.all([
  fetch("/api/users").then(r => r.json()),
  fetch("/api/orders").then(r => r.json())
]);

The event loop, simply

The call stack runs your code one frame at a time. When you start something asynchronous, the host environment — the browser — takes over the waiting. When it’s done, your callback goes into a queue.

The event loop checks whether the stack is empty and, if so, takes work from the queues. Promise callbacks go on the microtask queue, which is drained completely before the next task; timer and event callbacks go on task queues. That’s why a resolved promise’s .then() runs before a setTimeout(..., 0) registered earlier.

It also explains why long synchronous work freezes a page: nothing else can run until the stack clears. This is a simplified model — real implementations have more detail — but it’s enough to reason about ordering.

Call stack runs your code
Async work handed to the host environment
Callback queued: microtask or task
Event loop waits for an empty stack
Microtasks drained first, then a task
Callback runs on the stack
Simplified event loop showing the call stack, host environment, queues and the event loop
ILLUSTRATIVE API WORKFLOW
User action in the interface
JavaScript calls fetch()
HTTP request — GET /api/products
Server handles the request
JSON response
JavaScript parses and validates it
DOM updated with the data
JavaScript API request and JSON response workflow
 JavaScript — fetch and HTTP methodsEXAMPLE
// GET with proper error checking
async function getProducts() {
  const response = await fetch("/api/products");

  // fetch does NOT reject on 404 or 500
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

// POST with a JSON body
async function createProduct(product) {
  const response = await fetch("/api/products", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(product)
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

APIS

Fetch, HTTP and CORS

fetch() is the browser’s API for network requests. It returns a promise resolving to a Response object, from which response.json() (itself asynchronous) extracts the parsed body.

One behaviour catches almost everyone: fetch does not reject for HTTP error statuses. A 404 or 500 still fulfils the promise — the request succeeded, the server just answered unhappily. Only network failures reject. Always check response.ok or response.status yourself.

HTTP methods express intent: GET retrieves, POST creates, PUT replaces, PATCH updates part of a resource, DELETE removes. Sending JSON means setting a Content-Type header and stringifying the body.

CORS (Cross-Origin Resource Sharing) governs whether browser JavaScript may read a response from a different origin. It’s a browser policy, not a server firewall — the request often reaches the server regardless; the browser blocks your script from reading the result unless the server sends permitting headers. Certain requests trigger a preflight OPTIONS check first. CORS errors are fixed on the server, not in your fetch call. More in our website APIs guide.

MODULES

JavaScript Modules

Modules split code into files with explicit dependencies. export makes something available; import brings it in. Each module has its own scope, so nothing leaks into the global namespace unless exported.

Named exports let a module export several things, imported by name in braces — good for utility collections, and friendlier to tooling that removes unused code. A default export is the module’s single main thing, imported without braces under any name you choose. Mixing both is possible; picking one convention per project is clearer.

The benefits compound as a project grows: clearer organisation, genuine reuse, encapsulated internals, obvious dependencies and far easier maintenance. In the browser, modules load with <script type="module">, which is deferred by default and runs in strict mode.

EXAMPLE MODULE STRUCTURE
app.js
↓
ui.js
DOM rendering
api.js
fetch logic
↓
Application
Illustrative JavaScript module structure with app, UI and API modules
 JavaScript — modulesEXAMPLE
// math.js — named exports
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;

// logger.js — default export
export default function log(message) {
  console.log(`[app] ${message}`);
}

// app.js — importing both kinds
import log from "./logger.js";
import { add, PI } from "./math.js";
import * as math from "./math.js";

log(`Total: ${add(2, 3)}`);

BROWSER STORAGE

localStorage, sessionStorage and Cookies

The browser offers several ways to keep data on the client, each with a different lifetime and purpose. None of them are part of the JavaScript language — they’re Web APIs.

localStorage persists across sessions until cleared; sessionStorage lives only as long as the tab. Both store strings, so objects need JSON.stringify() going in and JSON.parse() coming out, and both can throw — in private browsing modes or when quota is exceeded — so wrap access in try/catch.

Cookies are older and behave differently: they’re sent to the server with matching requests, which is why sessions use them. The security attributes matter — HttpOnly cookies cannot be read by client-side JavaScript, which is precisely the point for session tokens, while Secure restricts them to HTTPS and SameSite limits cross-site sending.

A rule worth taking seriously: don’t put authentication secrets in localStorage. Any script running on the page can read it, including a compromised third-party dependency. Session tokens belong in cookies with appropriate flags, set by the server.

Aspect
localStorage
sessionStorage
Cookies
Lifetime
localStoragePersists until cleared
sessionStorageCleared when the tab closes
CookiesCan have an expiry date
Scope
localStoragePer origin, shared across tabs
sessionStoragePer tab
CookiesPer domain and path
Sent to server
localStorageNo
sessionStorageNo
CookiesYes, with every matching request
Capacity
localStorageSeveral megabytes typically
sessionStorageSimilar to localStorage
CookiesAround 4KB
JavaScript access
localStorageYes
sessionStorageYes
CookiesOnly if not HttpOnly
Suited to
localStoragePreferences, cached non-sensitive data
sessionStorageTemporary per-tab state
CookiesSessions and server-read flags
Comparison of localStorage, sessionStorage and cookies
 JavaScript — browser storageEXAMPLE
// Storing and reading preferences
try {
  localStorage.setItem("theme", "dark");
  const theme = localStorage.getItem("theme");
} catch (error) {
  // Storage can be unavailable or full
  console.warn("Storage unavailable", error);
}

// Objects must be serialised
localStorage.setItem("filters", JSON.stringify({ sort: "new" }));
const filters = JSON.parse(localStorage.getItem("filters") ?? "{}");

// Per-tab state
sessionStorage.setItem("step", "2");

localStorage.removeItem("theme");

ERRORS & DEBUGGING

Error Handling and Debugging JavaScript

try runs code that might fail, catch receives the error, finally runs either way, and throw raises your own error. Catching an error and doing nothing with it is worse than not catching it — failures become invisible. Unhandled promise rejections are the async equivalent, so give every chain a .catch() or wrap awaits in try/catch.

Common error types tell you where to look. SyntaxError means the code couldn’t be parsed. ReferenceError means a name doesn’t exist in scope — a typo, or the temporal dead zone. TypeError means a value isn’t what you expected, and is the one behind “cannot read properties of undefined”. RangeError means a value is outside an allowed range.

For DevTools: the Console shows errors and lets you evaluate expressions; Sources sets breakpoints, steps through code and watches expressions; Network shows requests, statuses and payloads; Application inspects storage and cookies; Performance profiles long tasks. Panel names and layouts differ between browsers.

01

Reproduce

Find the exact steps that trigger it.

02

Read the Error

The message and type usually name the problem.

03

Inspect the Stack Trace

Follow it to the line where things went wrong.

04

Use the Console

Log values around the failure point.

05

Set a Breakpoint

Pause execution in the Sources panel.

06

Inspect Variables

Check what the values actually are, not what you assume.

07

Test Assumptions

Confirm each step behaves as expected.

08

Fix

Address the cause rather than the symptom.

09

Retest

Verify the fix and check nothing else broke.

 JavaScript — error handlingEXAMPLE
try {
  const data = JSON.parse(input);
  processData(data);
} catch (error) {
  if (error instanceof SyntaxError) {
    showMessage("That data could not be read.");
  } else {
    showMessage("Something went wrong.");
  }
  console.error(error);
} finally {
  hideSpinner();
}

// Throwing your own errors
function getUser(id) {
  if (!id) {
    throw new Error("getUser requires an id");
  }
}

// Never leave a promise chain unhandled
loadData().catch(error => console.error(error));
 JavaScript — debouncing and throttlingEXAMPLE
// Debounce: run once after activity stops
function debounce(fn, delay = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

searchInput.addEventListener(
  "input",
  debounce(event => runSearch(event.target.value), 300)
);

// Throttle: run at most once per interval
function throttle(fn, interval = 200) {
  let ready = true;
  return (...args) => {
    if (!ready) return;
    ready = false;
    fn(...args);
    setTimeout(() => { ready = true; }, interval);
  };
}

window.addEventListener("scroll", throttle(updateHeader, 200));

PERFORMANCE

JavaScript Performance and Memory

JavaScript is often the heaviest part of a modern page, and unlike images it costs twice — once to download, again to parse and execute on the main thread.

The usual culprits: shipping more code than the page needs, large bundles, long tasks that block interaction, excessive DOM updates in loops, listeners attached to hundreds of elements and unnecessary re-rendering. Useful responses include code splitting so each page loads only what it uses, lazy loading non-critical features, batching DOM writes, and event delegation instead of many listeners.

Debouncing waits until activity stops before running — right for search-as-you-type and resize handling. Throttling runs at most once per interval — right for scroll and pointer movement, which fire continuously.

Memory is managed automatically: garbage collection reclaims anything no longer reachable, and you don’t control when it runs. What you do control is reachability — listeners never removed, timers never cleared, detached DOM nodes still referenced and ever-growing caches all keep memory alive unnecessarily. Performance depends on the whole application, not any single technique; see website speed.

ACCESSIBILITY & SECURITY

Accessible and Secure JavaScript

JavaScript and accessibility

Custom interactive UI is where accessibility most often breaks, because JavaScript can produce controls that look interactive without behaving like it.

Start from semantic HTML: a real <button> brings focusability, keyboard activation and the correct role for free, while a clickable <div> brings none. Beyond that, manage focus deliberately — move it into a dialog when it opens and back to the trigger when it closes — keep focus visible, support keyboard interaction including Escape to close, give icon-only controls accessible names, and update ARIA states such as aria-expanded whenever the visual state changes.

Announce dynamic changes where they matter, using a live region for content that updates without a page change, and make form errors specific and associated with their field. Test with a keyboard first — it catches most problems in minutes. See website accessibility.

JavaScript security basics

Cross-site scripting (XSS) happens when untrusted input is inserted in a way the browser treats as markup or script. The defensive habit is simple: use textContent for text, and treat innerHTML as something that requires justification. If you must render HTML from user content, sanitise it with a maintained library rather than your own regex.

Beyond that: never trust client-side data, since validation can be bypassed — validate on the server. Keep secrets out of frontend code entirely; anything shipped to the browser is public. Prefer HttpOnly cookies over localStorage for session tokens. Understand that CORS protects users, not your API — it isn’t authorisation. A Content Security Policy limits what can execute, and every third-party script you add runs with full access to your page, so keep dependencies few and updated.

 JavaScript — safer DOM updatesEXAMPLE
// Risky with untrusted content
// element.innerHTML = userInput;

// Safe: treated as text, never parsed as markup
element.textContent = userInput;

// Building markup from data, safely
function renderItem(product) {
  const li = document.createElement("li");
  const title = document.createElement("strong");

  title.textContent = product.name;   // safe
  li.append(title);
  return li;
}

// Validate API data before trusting its shape
if (!Array.isArray(data?.products)) {
  throw new Error("Unexpected response shape");
}

WORKING TOGETHER

Progressive Enhancement: JavaScript With HTML and CSS

Progressive enhancement means core content and functionality work from HTML, with CSS and JavaScript layering improvements on top. Scripts fail more often than people expect — a network hiccup, a blocked third party, an unsupported feature — and a page whose navigation or main content depends entirely on JavaScript has no fallback when they do.

It doesn’t mean avoiding JavaScript. It means asking whether HTML already solves the problem. A link navigates without script. A form submits without script. <details> gives an accessible disclosure widget with no code at all. Use JavaScript to improve those things, not to reimplement them.

The healthiest pattern for state is the one shown here: JavaScript changes an attribute or class, and CSS decides what that looks like. Styling stays where it can be maintained, and the accessibility state and the visual state can’t drift apart.

 HTML, CSS and JavaScript togetherEXAMPLE
<!-- HTML: real semantics and state -->
<button type="button" class="menu-button" aria-expanded="false"
        aria-controls="main-menu">Menu</button>
<nav id="main-menu" class="menu" hidden>...</nav>

/* CSS: presentation reacts to state */
.menu-button[aria-expanded="true"] { background: #0D4A8A; }
.menu.is-open { display: block; }

// JavaScript: only changes state
const button = document.querySelector(".menu-button");
const menu = document.getElementById("main-menu");

button.addEventListener("click", () => {
  const open = button.getAttribute("aria-expanded") === "true";

  button.setAttribute("aria-expanded", String(!open));
  menu.classList.toggle("is-open", !open);
  menu.hidden = open;
});
 JavaScript — classes, prototypes and generatorsEXAMPLE
class User {
  #secret = "private field";      // truly private

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

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

  get label() {
    return this.name.toUpperCase();
  }
}

class Admin extends User {
  constructor(name, level) {
    super(name);
    this.level = level;
  }

  greet() {
    return `${super.greet()} (admin)`;
  }
}

const admin = new Admin("Alex", 2);
admin.greet();

// Classes are syntax over prototypes
Object.getPrototypeOf(admin) === Admin.prototype;  // true

// Generators produce values on demand
function* idGenerator() {
  let id = 1;
  while (true) yield id++;
}

const ids = idGenerator();
ids.next().value;  // 1

OBJECT MODEL

Classes, Prototypes and Generators

Classes provide familiar syntax for creating objects with shared behaviour: a constructor that runs on new, methods, getters and setters, and extends with super for inheritance. Private fields use a # prefix.

Underneath, JavaScript remains prototype-based. Every object has a link to a prototype object, and when you access a property the engine walks that prototype chain until it finds it or reaches the end. Class methods live on the prototype, so all instances share one copy. Classes are a syntax layer over this model, not a replacement for it — which is why understanding prototypes still pays off when debugging.

Iterators and generators are a more advanced corner. A generator function, written function*, can pause at each yield and resume later, producing values on demand rather than all at once. They’re useful for lazy sequences and custom iteration; most day-to-day code doesn’t need them.

MODERN JS & RUNTIMES

Modern JavaScript and Where It Runs

These features are all part of modern JavaScript and appear throughout this page. They’re listed, not ranked — which ones you reach for depends on the problem.

let and const
Arrow functions
Template literals
Destructuring
Spread syntax
Rest parameters
Modules
Promises
async/await
Optional chaining
Nullish coalescing
Classes and private fields
Map and Set
Iterators and generators

Browser

The browser provides the DOM, fetch, storage, timers, events and many other Web APIs on top of the language. This is the environment most of this page describes.

Node.js

A JavaScript runtime outside the browser, used for server applications, APIs, command-line tools and build tooling. It has no DOM; instead it offers file system, networking and process APIs. Compare with PHP and Laravel for server-side work.

Other Runtimes

Alternative server runtimes, edge environments and embedded engines also execute JavaScript with their own host APIs. The language travels; the surrounding capabilities don't.

The consistent principle: ECMAScript defines the language; the runtime provides the capabilities. Code that assumes document exists won’t run on a server, and code using the file system won’t run in a browser. Keeping that boundary in mind prevents a lot of confusion when moving between environments, and it explains why some “JavaScript” answers online simply don’t apply to your context. Browser Web APIs are explored further in our website APIs guide.

WORDPRESS & ELEMENTOR

JavaScript in WordPress and Elementor

A WordPress page usually runs JavaScript from several sources at once: the theme, plugins, Elementor’s own frontend scripts, and anything you add. When something breaks, the console is the first place to look — one uncaught error can stop unrelated scripts from running.

WordPress uses JavaScript extensively: the block editor is a JavaScript application, the REST API serves data to frontend and admin interfaces, and older AJAX endpoints remain common in plugins. Scripts should be enqueued properly so dependencies and load order are managed for you.

In Elementor, custom JavaScript is useful for genuinely custom behaviour — calculators, bespoke form interactions, dynamic interfaces, API integrations, custom widgets. It isn’t useful for recreating tabs, accordions or menus that Elementor already provides accessibly; that’s more code to maintain for no gain. Target your own added CSS classes rather than generated element IDs, which change, and never edit Elementor’s core JavaScript — updates will overwrite it. Test custom scripts on desktop and mobile, and after plugin updates.

Use Enqueued Scripts

Load JavaScript through WordPress's proper mechanisms so dependencies and order are handled.

Avoid Global Pollution

Scope your code in modules or an IIFE so it can't clash with themes and plugins.

Use Event Delegation

Handle dynamically added elements without re-attaching listeners.

Avoid Inline JavaScript

Keep behaviour out of markup and in maintainable files.

Test Plugin Conflicts

Check the console; a single error can stop other scripts running.

Optimise Script Loading

Defer non-critical work rather than blocking the main thread.

Never Trust Client Data

Validate and sanitise on the server, always.

Theme scripts
Plugin scripts
Elementor frontend scripts
Your custom JavaScript
Browser console: the first place to debug
Sources of JavaScript on a typical WordPress page
 JavaScript — safe custom script patternEXAMPLE
// Wait for the DOM, scope your code, fail safely
document.addEventListener("DOMContentLoaded", () => {
  const widget = document.querySelector(".my-custom-widget");

  if (!widget) return;   // element not on this page

  widget.addEventListener("click", event => {
    const action = event.target.closest("[data-action]");
    if (!action) return;

    // handle the action
  });
});

IMPROVEMENTS

Better JavaScript: Practical Examples and Anti-Patterns

Simplified pairs — implementation context always matters — but these swaps address the problems that come up most often in review.

Common approach
Better approach
Why it helps
<button onclick="openMenu()">
Better approachbutton.addEventListener("click", openMenu)
Why it helpsBehaviour stays out of markup and can be removed later
element.innerHTML = userInput;
Better approachelement.textContent = userInput;
Why it helpsContent can't be parsed as markup or script
Nested callbacks several levels deep
Better approachasync/await with try/catch
Why it helpsReads top to bottom, errors handled in one place
if (count || 10)
Better approachif (count ?? 10)
Why it helpsA legitimate zero isn't replaced by the default
Repeated fetch logic in every file
Better approachOne reusable API module
Why it helpsError handling and headers defined once
Listener on every list item
Better approachOne delegated listener on the parent
Why it helpsWorks for items added later, fewer listeners
Comparison of common JavaScript patterns with better alternatives

Common JavaScript mistakes to avoid

Excessive Global Variables

Names collide across themes, plugins and scripts.

Giant Functions

One function doing five jobs is untestable and unreadable.

Deep Callback Nesting

Promises or async/await flatten it out.

Ignored Promise Rejections

Failures disappear silently and confuse users.

Missing Error Handling

Assuming every request succeeds.

Unsafe innerHTML

The most common route to cross-site scripting.

Trusting Client Validation

Anyone can bypass it; the server must check.

Blocking the Main Thread

Long tasks freeze the entire interface.

Excessive DOM Updates

Writing in a loop instead of batching.

Memory Leaks

Listeners and timers that are never cleaned up.

Unnecessary Listeners

Hundreds where delegation needs one.

Mixed Responsibilities

UI, data and logic tangled in one file.

Hard-Coded Data

Values that should come from an API or config.

Exposed Secrets

Anything in frontend code is public.

Ignoring Accessibility

Custom controls that keyboards can't reach.

Rebuilding HTML Semantics

Divs where a button or link would do.

Overengineering

A framework for a job three lines could handle.

Not Testing Mobile

Touch behaviour and performance differ.

REFERENCE

JavaScript Cheat Sheet

A categorised quick reference for the syntax and APIs covered above. For authoritative definitions, use MDN Web Docs; for the language specification itself, ECMA-262 and the TC39 proposals.

VARIABLESletconstvar
CONDITIONSifelseswitch?:???.
LOOPSforwhilefor...offor...in
FUNCTIONSfunction=>return...rest
ARRAYSmap()filter()reduce()find()some()every()includes()
OBJECTSobj.propobj["prop"]{...spread}destructuring
DOMquerySelector()querySelectorAll()getElementById()textContentclassList
EVENTSaddEventListener()event.targetpreventDefault()
ASYNCPromiseasyncawait.then().catch()fetch()
STORAGElocalStoragesessionStorage
MODULESimportexportexport default
ERRORStrycatchfinallythrow
Categorised JavaScript reference covering variables, conditions, loops, functions, arrays, objects, DOM, events, async, storage, modules and errors

INTERVIEW PREP

Common JavaScript Interview Questions

Concise, technically accurate answers to the JavaScript questions that come up most often in interviews and technical assessments.

CHECKLISTS

JavaScript Accessibility, Performance and Security Checklists

Four review passes over the same code. Practical working lists rather than formal audits — the accessibility one is not a complete WCAG assessment, and the security one is a starting point rather than a threat model.

Accessibility

Performance

Security

Code quality

 JavaScript quality checklistEXAMPLE
ACCESSIBILITY
[ ] Keyboard interaction works
[ ] Focus is managed correctly
[ ] Focus remains visible
[ ] Interactive controls use appropriate HTML
[ ] Dynamic content is announced where appropriate
[ ] Dialogs manage focus
[ ] Menus support keyboard interaction
[ ] Form errors are understandable
[ ] Native semantics are not removed unnecessarily
[ ] ARIA states are updated when needed
[ ] Hover is not the only interaction
[ ] Motion respects user preferences
[ ] Important content does not depend only on JavaScript
[ ] Screen-reader behaviour is tested
[ ] Mobile interaction is tested

PERFORMANCE
[ ] Avoid unnecessary JavaScript
[ ] Reduce large bundles
[ ] Load scripts appropriately
[ ] Avoid long tasks
[ ] Avoid unnecessary DOM updates
[ ] Use event delegation where useful
[ ] Debounce expensive input events
[ ] Throttle continuous events
[ ] Lazy-load non-critical functionality
[ ] Split code when useful
[ ] Avoid memory leaks
[ ] Optimise API requests
[ ] Cache appropriate data
[ ] Test on real devices
[ ] Monitor performance after changes

SECURITY
[ ] Never trust client-side input
[ ] Avoid unsafe HTML insertion
[ ] Validate data server-side
[ ] Protect sensitive information
[ ] Use secure authentication mechanisms
[ ] Understand what CORS does and doesn't do
[ ] Consider a Content Security Policy
[ ] Keep dependencies updated
[ ] Keep secrets out of frontend code
[ ] Use HTTPS
[ ] Review third-party scripts

CODE QUALITY
[ ] Code uses meaningful names
[ ] Functions have focused responsibilities
[ ] Scope is controlled
[ ] Modules used where helpful
[ ] Errors are handled
[ ] API responses are validated
[ ] User input is not trusted
[ ] DOM updates are efficient
[ ] Events handled appropriately
[ ] Accessibility is preserved
[ ] Performance is tested
[ ] Browser compatibility considered
[ ] Tested on mobile
[ ] Console errors resolved

BEST PRACTICES

JavaScript Best Practices

Use Meaningful Names

Code is read far more often than it's written.

Prefer const

Signal intent; reach for let only when reassignment is needed.

Keep Functions Focused

One job per function makes testing and reuse possible.

Use Modules

Separate responsibilities into files with clear dependencies.

Handle Errors

Never assume a request or a parse will succeed.

Validate External Data

APIs and users both supply unexpected shapes.

Prefer Semantic HTML

Enhance real elements rather than rebuilding them.

Avoid Globals

Keep scope controlled to prevent collisions.

Comment Why, Not What

Explain non-obvious decisions, not obvious syntax.

Test Real User Flows

Isolated functions passing isn't the same as the feature working.

LEARNING PATH

JavaScript Learning Roadmap and Practice Projects

A workable order for learning JavaScript. Frameworks sit last on purpose — React and similar libraries assume comfort with functions, arrays, objects, modules and async code, and learning them first tends to hide gaps rather than fill them.

JavaScript basics
Variables
Data types
Operators
Conditions
Loops
Functions
Arrays
Objects
Scope and closures
DOM
Events
Forms
Async JavaScript
Promises
async/await
Fetch and APIs
Modules
Browser storage
Error handling
Debugging
Accessibility
Performance
Security
Projects
Frameworks

These are projects to build yourself rather than templates to download. Each isolates a specific set of techniques, and each will surface problems that reading alone never does — especially around error states and edge cases.

Counter App

Beginner — variables, events and DOM updates.

Digital Clock

Beginner — dates, timers and formatting.

Interactive FAQ

Beginner — classList, ARIA states and keyboard support.

To-Do List

Intermediate — arrays, rendering and localStorage.

Form Validation

Intermediate — events, validation and accessible errors.

Image Gallery

Intermediate — delegation, focus management and dialogs.

Search Filter

Intermediate — array methods and debouncing.

API Dashboard

Advanced — fetch, async/await and error states.

Product Search

Advanced — API data, filtering and state.

Expense Tracker

Advanced — modules, storage and reduce().

HTML structure
CSS styling
JavaScript logic
Events
State
API and data
Error handling
Accessibility
Testing
Performance
Publish
JavaScript project workflow from HTML structure through logic, state, data, error handling, accessibility and testing to publishing

NEXT TOPICS

Continue Learning Web Development

HTML

Learn how HTML structures content with semantic elements, links, forms and media.

CSS

Learn how CSS controls layout, typography, responsive design and visual styling.

PHP

Explore server-side development and dynamic web applications.

React

Learn component-based frontend development built on JavaScript.

Laravel

Explore PHP-based web application development.

Website APIs

Understand how websites and applications exchange data with external services.

Website Accessibility

Learn how JavaScript-powered interactions can be made accessible.

Responsive Design

Learn how HTML, CSS and JavaScript work together across screen sizes.

FAQ

JavaScript: Frequently Asked Questions

Short answers to the questions people ask most about JavaScript.

Build Better Web Experiences With JavaScript

Learn how JavaScript brings websites to life through interaction, DOM manipulation, asynchronous programming, APIs, reusable modules and modern web application techniques.