JavaScript:
Absolute Beginner
to Production-Ready
A comprehensive, project-based guide by a Senior JavaScript Engineer. Practical, modern, and structured like a real-world developer bootcamp — from your first console.log to shipping production code.
Introduction to JavaScript
JavaScript (JS) is the world's most widely used programming language. Originally built to make web pages interactive — button clicks, form validation, dropdown menus — today it runs everywhere: browsers, servers (Node.js), mobile apps (React Native), and desktop apps (Electron).
Every browser ships with a JS engine — a program that reads and runs your JavaScript. V8 uses JIT (Just-In-Time) compilation — it compiles JS to machine code at runtime for speed.
| Browser | Engine | Note |
|---|---|---|
| Chrome / Edge | V8 | Also used in Node.js — most dominant |
| Firefox | SpiderMonkey | Mozilla's engine, highly standards-compliant |
| Safari | JavaScriptCore (Nitro) | Apple's engine for WebKit |
JavaScript can access and modify the DOM (Document Object Model) — a live tree representation of the HTML page. It can also read and write CSS styles dynamically.
<!-- Always load scripts at the bottom of <body> or use defer --> <body> <h1 id="title">Hello, World!</h1> <button id="btn">Click me</button> <script src="app.js"></script> </body>
const btn = document.getElementById('btn'); btn.addEventListener('click', function () { document.getElementById('title').textContent = 'You clicked it!'; });
Download from nodejs.org — this gives you node and npm commands.
Download from code.visualstudio.com. Install the ESLint and Prettier extensions.
mkdir my-js-project cd my-js-project code . # opens VS Code in this folder # Verify Node.js is installed: node --version # e.g., v22.0.0 npm --version # e.g., 10.8.0 node # opens the REPL — try: console.log("Hello!")
JavaScript Basics — The Foundation
const by default. Switch to let only when you need to reassign. Never use var.// ❌ var — OLD way. Avoid it. Has confusing scoping rules. var oldStyle = "avoid me"; // ✅ let — for values that will CHANGE let score = 0; score = 10; score = score + 5; // score is now 15 // ✅ const — for values that will NOT change (use this by default!) const PI = 3.14159; const APP_NAME = "MyApp"; // PI = 3; // ❌ TypeError: Assignment to constant variable // Why not var? — var is function-scoped, NOT block-scoped if (true) { var leakyVar = "I leak outside the block!"; let safeVar = "I stay inside."; } console.log(leakyVar); // "I leak!" — unexpected bug! console.log(safeVar); // ❌ ReferenceError — correct behavior
// String — text, always in quotes const firstName = "Alice"; const template = `Hi, ${firstName}!`; // Template literal (preferred) // Number — integers and decimals const age = 28; const price = 9.99; const notANumber = NaN; // result of invalid math: "abc" * 2 // Boolean const isLoggedIn = true; // null — intentional absence of value (you set this) const selectedItem = null; // undefined — variable declared but not assigned let userInput; console.log(userInput); // undefined // BigInt — for integers larger than 2^53 - 1 const bigNumber = 9007199254740991n; // Checking types typeof "hello" // "string" typeof 42 // "number" typeof null // "object" ⚠️ famous JS bug — null is NOT an object typeof [] // "object" — arrays are objects too
typeof null === "object" is a well-known JavaScript bug that exists for historical reasons. Always use === null to check for null directly.
// Primitives: copied by VALUE let a = 10; let b = a; // b gets a copy of the value b = 20; console.log(a); // 10 — a is unchanged ✅ // Objects: copied by REFERENCE const obj1 = { name: "Alice" }; const obj2 = obj1; // obj2 points to the SAME object in memory! obj2.name = "Bob"; console.log(obj1.name); // "Bob" — obj1 was also changed! 😱 // ✅ To make a true copy, use the spread operator: const obj3 = { ...obj1 }; // independent copy
// ✅ Always use === (strict equality) — checks VALUE and TYPE 5 === 5 // true 5 === "5" // false — different types! 5 == "5" // true — ⚠️ AVOID loose equality, coerces types // Logical operators const isAdmin = true; const isActive = false; isAdmin && isActive; // false — AND (both must be true) isAdmin || isActive; // true — OR (at least one true) !isAdmin; // false — NOT // Short-circuit: if username is falsy, use "Guest" const username = null; const displayName = username || "Guest"; // "Guest" // Falsy values: false, 0, "", null, undefined, NaN // Everything else is truthy
Control Flow
const temperature = 22; if (temperature > 30) { console.log("Hot! Wear sunscreen."); } else if (temperature > 20) { console.log("Warm. T-shirt is fine."); // ← this runs } else { console.log("Cold! Wear a coat."); } // Ternary operator — shorthand for simple if/else const access = temperature > 20 ? "Enjoy outdoors!" : "Stay inside."; console.log(access); // "Enjoy outdoors!"
// for loop — when you know the number of iterations for (let i = 1; i <= 5; i++) { console.log(`Iteration: ${i}`); } // for...of — cleaner array iteration (preferred!) const fruits = ["apple", "banana", "cherry"]; for (const fruit of fruits) { console.log(fruit); } // for...in — iterate over object keys const person = { name: "Alice", age: 28, city: "Kigali" }; for (const key in person) { console.log(`${key}: ${person[key]}`); } // while — when you don't know the count upfront let attempts = 0; while (attempts < 3) { console.log(`Attempt ${attempts + 1}`); attempts++; } // break & continue for (let i = 0; i < 10; i++) { if (i === 5) break; // stop at 5 if (i % 2 === 0) continue; // skip even numbers console.log(i); // prints: 1, 3 }
Functions
// DECLARATION — hoisted: can be called before it's defined function greet(name) { return `Hello, ${name}!`; } // ARROW FUNCTIONS (ES6) — modern standard const add = (a, b) => a + b; // implicit return const double = n => n * 2; // single param, no parens needed const sayHi = () => "Hi!"; // no params, empty parens required // Default parameters function createUser(name, role = "viewer", isActive = true) { return { name, role, isActive }; // shorthand: name: name } createUser("Alice", "admin"); // { name: "Alice", role: "admin", isActive: true } createUser("Bob"); // { name: "Bob", role: "viewer", isActive: true }
A closure is when an inner function "remembers" variables from its outer scope even after the outer function has finished executing. This is one of JS's most powerful — and initially confusing — features.
function makeCounter() { let count = 0; // this variable is "enclosed" in the returned function return function () { count++; return count; }; } const counter = makeCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 // Each makeCounter() call creates an INDEPENDENT closure const counterA = makeCounter(); const counterB = makeCounter(); counterA(); // 1 counterA(); // 2 counterB(); // 1 ← independent! // Real-world: factory functions function createMultiplier(factor) { return (number) => number * factor; // closes over 'factor' } const triple = createMultiplier(3); const quadruple = createMultiplier(4); console.log(triple(5)); // 15 console.log(quadruple(5)); // 20
Arrays & Objects
const products = [ { id: 1, name: "Laptop", price: 999, inStock: true }, { id: 2, name: "Mouse", price: 29, inStock: true }, { id: 3, name: "Monitor", price: 399, inStock: false }, { id: 4, name: "Keyboard",price: 79, inStock: true }, ]; // map() — transform every element, returns a new array SAME length const names = products.map(p => p.name); // ["Laptop", "Mouse", "Monitor", "Keyboard"] // filter() — keep only elements that pass a test const available = products.filter(p => p.inStock); // 3 products (Laptop, Mouse, Keyboard) // reduce() — accumulate all elements into a SINGLE value const total = products.reduce((sum, p) => sum + p.price, 0); console.log(total); // 1506 // forEach() — side effects only, returns NOTHING products.forEach(p => { if (!p.inStock) console.log(`⚠️ ${p.name} out of stock`); });
const user = { name: "Alice", age: 28, city: "Kigali", role: "admin" }; // Object destructuring const { name, age } = user; // extract specific keys const { name: userName } = user; // rename while destructuring // Array destructuring const [first, second, , fourth] = [10, 20, 30, 40]; // skip index 2 // Swap without temp variable let x = 1, y = 2; [x, y] = [y, x]; // SPREAD operator — expand an iterable const a = [1, 2, 3]; const copy = [...a]; // independent copy const merged = [...a, 4, 5]; // [1, 2, 3, 4, 5] const prodConfig = { ...{ host: "localhost", port: 3000 }, host: "api.myapp.com" }; // { host: "api.myapp.com", port: 3000 } // REST operator — collect remaining into array function sum(...nums) { return nums.reduce((total, n) => total + n, 0); } sum(1, 2, 3, 4, 5); // 15 const [head, ...tail] = [1, 2, 3, 4]; // head = 1, tail = [2, 3, 4]
DOM Manipulation
// Selecting elements const title = document.getElementById("title"); const btn = document.querySelector("#submit-btn"); // first match const cards = document.querySelectorAll(".card"); // all matches const cardArray = [...cards]; // convert NodeList to true array // Content & attributes title.textContent = "Safe text (no HTML parsing)"; title.innerHTML = "Text with <strong>bold</strong>"; // ⚠️ XSS risk // CSS classes (preferred over inline styles) title.classList.add("active"); title.classList.remove("hidden"); title.classList.toggle("dark-mode"); // add if missing, remove if present title.classList.contains("active"); // true/false // Dataset attributes — data-* in HTML // <button data-user-id="42" data-role="admin">Edit</button> const editBtn = document.querySelector("button"); console.log(editBtn.dataset.userId); // "42" console.log(editBtn.dataset.role); // "admin"
// Click, input, submit events btn.addEventListener("click", (event) => { console.log("Clicked!", event.target); }); form.addEventListener("submit", (event) => { event.preventDefault(); // ← CRITICAL: prevents page reload const data = Object.fromEntries(new FormData(event.target)); console.log(data); }); // EVENT DELEGATION — ONE listener for MANY elements // Instead of adding listeners to each item, add one to the parent document.querySelector("#product-list").addEventListener("click", (e) => { const item = e.target.closest(".product-item"); if (item) console.log("Product ID:", item.dataset.id); });
Asynchronous JavaScript
JavaScript is single-threaded but handles async tasks through the event loop. Understanding this model is critical for writing performant, bug-free JS.
console.log("1 — synchronous"); setTimeout(() => console.log("2 — timeout (0ms)"), 0); // queued, not immediate! Promise.resolve().then(() => console.log("3 — promise microtask")); console.log("4 — synchronous"); // Output: 1, 4, 3, 2 // Microtasks (Promises) run BEFORE macrotasks (setTimeout)
fetchUser(id, (err, user) => { fetchPosts(user.id, (err, posts) => { fetchComments(posts[0].id, (err, comments) => { // deeply nested 😰 } ); }); });
async function load(id) { const user = await fetchUser(id); const posts = await fetchPosts(user.id); const coms = await fetchComments( posts[0].id); // flat, readable ✅ }
async function loadDashboard(userId) { try { const user = await fetchUser(userId); // must happen first // Run these CONCURRENTLY (3x faster than sequential!) const [orders, notifications] = await Promise.all([ fetchOrders(user.id), fetchNotifications(user.id) ]); return { user, orders, notifications }; } catch (error) { console.error("Dashboard failed:", error.message); throw error; } finally { // always runs — hide loading spinner, cleanup loadingEl.style.display = "none"; } }
Working with APIs
| Method | URL | Action | Success Code |
|---|---|---|---|
| GET | /api/users | Get all users | 200 OK |
| GET | /api/users/1 | Get user with ID 1 | 200 OK |
| POST | /api/users | Create a new user | 201 Created |
| PUT | /api/users/1 | Replace user 1 | 200 OK |
| PATCH | /api/users/1 | Partially update user 1 | 200 OK |
| DELETE | /api/users/1 | Delete user 1 | 204 No Content |
fetch() only rejects on network errors, NOT HTTP errors (404, 500). Always check response.ok!// GET request async function getUsers() { const response = await fetch("https://jsonplaceholder.typicode.com/users"); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); return response.json(); // parse JSON body } // POST request — send data async function createPost(postData) { const response = await fetch("https://jsonplaceholder.typicode.com/posts", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, body: JSON.stringify(postData) // JS object → JSON string }); if (!response.ok) { const err = await response.json(); throw new Error(err.message || "Failed to create post"); } return response.json(); // returns the created resource }
ES6+ Modern JavaScript
const user = { name: "Alice", address: { city: "Kigali" } }; // Optional chaining (?.) — safely access deep properties const city = user?.address?.city; // "Kigali" const phone = user?.phone?.number; // undefined (no error!) const firstUser = users?.[0]; // safe even if users is null // Nullish coalescing (??) — default for null/undefined ONLY const score1 = 0 || "No score"; // "No score" ← WRONG! 0 is valid const score2 = 0 ?? "No score"; // 0 ← CORRECT! const port = config?.port ?? 3000; const displayName = user?.nickname ?? user?.name ?? "Anonymous";
// math.js — exporting export const PI = 3.14159; export function add(a, b) { return a + b; } export default function subtract(a, b) { return a - b; } // default // main.js — importing import subtract, { add, PI } from "./math.js"; // named + default import * as MathUtils from "./math.js"; // namespace import console.log(add(2, 3)); // 5 console.log(MathUtils.PI); // 3.14159
Error Handling & Debugging
class ValidationError extends Error { constructor(message, field) { super(message); this.name = "ValidationError"; this.field = field; } } class NotFoundError extends Error { constructor(resource, id) { super(`${resource} with ID ${id} not found`); this.name = "NotFoundError"; this.statusCode = 404; } } // Differentiated error handling try { validateEmail("not-an-email"); } catch (error) { if (error instanceof ValidationError) { console.error(`Field '${error.field}': ${error.message}`); } else if (error instanceof NotFoundError) { console.error(`404: ${error.message}`); } else { console.error("Unexpected error:", error); // Report to Sentry, Datadog, etc. } }
async function loadData() { const data = fetch("/api/data"); // ❌ forgot await // data is a Promise, not JSON! console.log(data.users); // undefined }
async function loadData() { const res = await fetch("/api/data"); const data = await res.json(); // ✅ data is now the actual object console.log(data.users); }
JavaScript in the Browser
// localStorage persists FOREVER, sessionStorage clears on tab close localStorage.setItem("username", "alice"); localStorage.setItem("prefs", JSON.stringify({ theme: "dark" })); const username = localStorage.getItem("username"); // "alice" const prefs = JSON.parse(localStorage.getItem("prefs")); // object // Handle missing key gracefully — always provide a fallback const settings = JSON.parse(localStorage.getItem("settings") || "{}"); localStorage.removeItem("username"); localStorage.clear(); // ⚠️ removes ALL keys // Persist user theme preference function saveTheme(theme) { localStorage.setItem("theme", theme); document.body.classList.toggle("dark", theme === "dark"); } document.addEventListener("DOMContentLoaded", () => { const saved = localStorage.getItem("theme") || "light"; document.body.classList.toggle("dark", saved === "dark"); });
JavaScript with a Backend
const express = require("express"); const cors = require("cors"); const app = express(); app.use(cors()); // allow cross-origin requests app.use(express.json()); // parse incoming JSON bodies let products = [ { id: 1, name: "Laptop", price: 999, inStock: true }, ]; app.get("/api/products", (req, res) => { res.json(products); }); app.get("/api/products/:id", (req, res) => { const product = products.find(p => p.id === parseInt(req.params.id)); if (!product) return res.status(404).json({ message: "Not found" }); res.json(product); }); app.post("/api/products", (req, res) => { const { name, price, inStock = true } = req.body; if (!name || !price) return res.status(400).json({ message: "Required fields missing" }); const newProduct = { id: products.length + 1, name, price, inStock }; products.push(newProduct); res.status(201).json(newProduct); }); app.listen(3000, () => console.log("API running at http://localhost:3000"));
Mini Projects — Hands-On
📝 To-Do List App
Skills: DOM manipulation, event handling, LocalStorage persistence, filter/state management
// State let todos = JSON.parse(localStorage.getItem("todos") || "[]"); let currentFilter = "all"; function addTodo(text) { const trimmed = text.trim(); if (!trimmed) return; todos.push({ id: Date.now(), text: trimmed, completed: false }); localStorage.setItem("todos", JSON.stringify(todos)); render(); } function toggleTodo(id) { todos = todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t); localStorage.setItem("todos", JSON.stringify(todos)); render(); } function getFiltered() { if (currentFilter === "active") return todos.filter(t => !t.completed); if (currentFilter === "completed") return todos.filter(t => t.completed); return todos; } // Event delegation — ONE listener for all items document.getElementById("todo-list").addEventListener("click", (e) => { const id = Number(e.target.dataset.id); if (e.target.matches("input[type='checkbox']")) toggleTodo(id); if (e.target.matches("button")) todos = todos.filter(t => t.id !== id); localStorage.setItem("todos", JSON.stringify(todos)); render(); });
🌤️ Weather App (API Integration)
Skills: Fetch API, async/await, DOM rendering, error handling, search history
const GEO_API = "https://geocoding-api.open-meteo.com/v1/search"; const WEATHER_API = "https://api.open-meteo.com/v1/forecast"; // free, no key! async function searchWeather(cityName) { const errorEl = document.getElementById("error"); const loadingEl = document.getElementById("loading"); loadingEl.style.display = "block"; errorEl.style.display = "none"; try { // Step 1: city name → coordinates const geoRes = await fetch(`${GEO_API}?name=${encodeURIComponent(cityName)}&count=1`); const geoData = await geoRes.json(); if (!geoData.results?.length) throw new Error(`City "${cityName}" not found`); const { name, latitude, longitude, country_code } = geoData.results[0]; // Step 2: fetch weather with coordinates const params = new URLSearchParams({ latitude, longitude, current: "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code" }); const weatherRes = await fetch(`${WEATHER_API}?${params}`); const weather = await weatherRes.json(); renderWeather({ name, country_code }, weather); } catch (err) { errorEl.textContent = `❌ ${err.message}`; errorEl.style.display = "block"; } finally { loadingEl.style.display = "none"; } }
🛒 Full CRUD Products Manager
Skills: Full CRUD, API layer, state management, event delegation, modular code
// CENTRALIZED STATE const state = { products: [], editingId: null, filter: "", sortBy: "name" }; // API LAYER — all HTTP calls isolated here const api = { getAll: () => apiFetch("/api/products"), create: (data) => apiFetch("/api/products", { method: "POST", body: JSON.stringify(data) }), update: (id, data) => apiFetch(`/api/products/${id}`, { method: "PUT", body: JSON.stringify(data) }), delete: (id) => apiFetch(`/api/products/${id}`, { method: "DELETE" }), }; // COMPUTED STATE — derived values, not stored function getDisplayProducts() { return state.products .filter(p => p.name.toLowerCase().includes(state.filter.toLowerCase())) .sort((a, b) => state.sortBy === "price" ? a.price - b.price : a.name.localeCompare(b.name) ); } // ACTION — modify state + trigger render async function handleFormSubmit(event) { event.preventDefault(); const data = Object.fromEntries(new FormData(event.target)); if (state.editingId) { const updated = await api.update(state.editingId, data); state.products = state.products.map(p => p.id === updated.id ? updated : p); state.editingId = null; } else { const created = await api.create(data); state.products.push(created); } render(); // single render call }
Performance & Best Practices
// DEBOUNCE — wait until user stops typing function debounce(fn, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } const handleSearch = debounce(async (query) => { const results = await searchProducts(query); renderResults(results); }, 300); // waits 300ms after last keystroke searchInput.addEventListener("input", (e) => handleSearch(e.target.value)); // MEMOIZATION — cache expensive results function memoize(fn) { const cache = new Map(); return function (...args) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn.apply(this, args); cache.set(key, result); return result; }; } // LAZY LOADING — Intersection Observer for images const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; // load real image when in viewport observer.unobserve(img); } }); }); document.querySelectorAll("img[data-src]").forEach(img => observer.observe(img));
- All
console.log/debuggerremoved - API keys in environment variables, never in JS
- User-facing errors handled gracefully
- User input validated & sanitized (no raw innerHTML from users)
- All async functions have
try/catch - Event listeners cleaned up on unmount
constby default,letwhen needed,varnever- All
==replaced with=== - No blocking operations in render paths
- Images lazy-loaded with Intersection Observer
- Debounce on all input event handlers
- LocalStorage reads have JSON fallback (
|| "{}") - Code split into feature-based modules
- Concurrent async ops use
Promise.all()