Home › Web Development › JavaScript
WEB DEVELOPMENT / JAVASCRIPT
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.
THE BASICS
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.
THE FOUNDATION
Defines what content is and what it means, giving JavaScript the elements it selects and modifies. See our HTML guide.
Controls appearance and layout, including the state-based styling JavaScript triggers by toggling classes. See our CSS guide.
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
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.
// 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 = {}; // TypeErrorSYNTAX
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 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.
Text, in single quotes, double quotes or backticks.
Integers and decimals in one floating-point type.
Integers beyond the safe range of number.
true or false.
A variable declared but not assigned.
An intentional absence of a value.
Unique values, often used as special object keys.
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.
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 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.
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// 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
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
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.
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);// 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 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 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.
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);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 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 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.
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 (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.
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"));EVENTS
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.
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
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.
// 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 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.
// 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() 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
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.
// 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
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.
// 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
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.
Find the exact steps that trigger it.
The message and type usually name the problem.
Follow it to the line where things went wrong.
Log values around the failure point.
Pause execution in the Sources panel.
Check what the values actually are, not what you assume.
Confirm each step behaves as expected.
Address the cause rather than the symptom.
Verify the fix and check nothing else broke.
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));// 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 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
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.
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.
// 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 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: 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;
});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; // 1OBJECT MODEL
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
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.
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.
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
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.
Load JavaScript through WordPress's proper mechanisms so dependencies and order are handled.
Scope your code in modules or an IIFE so it can't clash with themes and plugins.
Handle dynamically added elements without re-attaching listeners.
Keep behaviour out of markup and in maintainable files.
Check the console; a single error can stop other scripts running.
Defer non-critical work rather than blocking the main thread.
Validate and sanitise on the server, always.
// 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
Simplified pairs — implementation context always matters — but these swaps address the problems that come up most often in review.
Names collide across themes, plugins and scripts.
One function doing five jobs is untestable and unreadable.
Promises or async/await flatten it out.
Failures disappear silently and confuse users.
Assuming every request succeeds.
The most common route to cross-site scripting.
Anyone can bypass it; the server must check.
Long tasks freeze the entire interface.
Writing in a loop instead of batching.
Listeners and timers that are never cleaned up.
Hundreds where delegation needs one.
UI, data and logic tangled in one file.
Values that should come from an API or config.
Anything in frontend code is public.
Custom controls that keyboards can't reach.
Divs where a button or link would do.
A framework for a job three lines could handle.
Touch behaviour and performance differ.
REFERENCE
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.
letconstvarifelseswitch?:???.forwhilefor...offor...infunction=>return...restmap()filter()reduce()find()some()every()includes()obj.propobj["prop"]{...spread}destructuringquerySelector()querySelectorAll()getElementById()textContentclassListaddEventListener()event.targetpreventDefault()Promiseasyncawait.then().catch()fetch()localStoragesessionStorageimportexportexport defaulttrycatchfinallythrowINTERVIEW PREP
Concise, technically accurate answers to the JavaScript questions that come up most often in interviews and technical assessments.
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 [ ] 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
Code is read far more often than it's written.
Signal intent; reach for let only when reassignment is needed.
One job per function makes testing and reuse possible.
Separate responsibilities into files with clear dependencies.
Never assume a request or a parse will succeed.
APIs and users both supply unexpected shapes.
Enhance real elements rather than rebuilding them.
Keep scope controlled to prevent collisions.
Explain non-obvious decisions, not obvious syntax.
Isolated functions passing isn't the same as the feature working.
LEARNING PATH
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.
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.
Beginner — variables, events and DOM updates.
Beginner — dates, timers and formatting.
Beginner — classList, ARIA states and keyboard support.
Intermediate — arrays, rendering and localStorage.
Intermediate — events, validation and accessible errors.
Intermediate — delegation, focus management and dialogs.
Intermediate — array methods and debouncing.
Advanced — fetch, async/await and error states.
Advanced — API data, filtering and state.
Advanced — modules, storage and reduce().
NEXT TOPICS
Learn how HTML structures content with semantic elements, links, forms and media.
Learn how CSS controls layout, typography, responsive design and visual styling.
Explore server-side development and dynamic web applications.
Learn component-based frontend development built on JavaScript.
Explore PHP-based web application development.
Understand how websites and applications exchange data with external services.
Learn how JavaScript-powered interactions can be made accessible.
Learn how HTML, CSS and JavaScript work together across screen sizes.
FAQ
Short answers to the questions people ask most about JavaScript.
Learn how JavaScript brings websites to life through interaction, DOM manipulation, asynchronous programming, APIs, reusable modules and modern web application techniques.