Ad – 728Γ—90
🎯 Interview Prep

JavaScript Practical Exercises – Build Real Things

The fastest way to solidify JavaScript skills is to build working things. This page contains 12 hands-on exercises progressing from beginner (temperature converter, counter) to advanced (pub/sub system, localStorage note app, infinite scroll). Every exercise includes complete, runnable code.

⏱️ 40 min read 🎯 Advanced πŸ“… Updated 2026

Beginner Exercises

These exercises focus on the fundamentals: DOM manipulation, events, and basic JavaScript logic. Each one can be built in a single HTML file.

Exercise 1: Temperature Converter

Build a converter that transforms between Celsius and Fahrenheit in real time as the user types.

What you learn: DOM querying, input events, real-time calculation, two-way binding logic.

JavaScript
// HTML needed:
// <input id="celsius" type="number" placeholder="Celsius">
// <input id="fahrenheit" type="number" placeholder="Fahrenheit">

const celsiusInput     = document.getElementById("celsius");
const fahrenheitInput  = document.getElementById("fahrenheit");

celsiusInput.addEventListener("input", () => {
  const c = parseFloat(celsiusInput.value);
  fahrenheitInput.value = isNaN(c) ? "" : ((c * 9) / 5 + 32).toFixed(2);
});

fahrenheitInput.addEventListener("input", () => {
  const f = parseFloat(fahrenheitInput.value);
  celsiusInput.value = isNaN(f) ? "" : (((f - 32) * 5) / 9).toFixed(2);
});
Behavior: Typing "100" in Celsius shows "212.00" in Fahrenheit. Typing "32" in Fahrenheit shows "0.00" in Celsius.

Exercise 2: Simple Counter

A counter with increment, decrement, and reset buttons. The display turns red when the count is negative.

What you learn: State management with a variable, conditional DOM updates, button event handling.

JavaScript
// HTML needed:
// <p id="count">0</p>
// <button id="inc">+</button>
// <button id="dec">-</button>
// <button id="reset">Reset</button>

let count = 0;
const display = document.getElementById("count");

function updateDisplay() {
  display.textContent = count;
  display.style.color = count < 0 ? "red" : count > 0 ? "green" : "inherit";
}

document.getElementById("inc").addEventListener("click", () => { count++; updateDisplay(); });
document.getElementById("dec").addEventListener("click", () => { count--; updateDisplay(); });
document.getElementById("reset").addEventListener("click", () => { count = 0; updateDisplay(); });

Exercise 3: Word Counter

Display live word count, character count, and estimated reading time as the user types in a textarea.

What you learn: String methods, regex, computed properties from user input.

JavaScript
// HTML needed:
// <textarea id="editor" rows="8"></textarea>
// <div id="stats"></div>

const editor = document.getElementById("editor");
const stats  = document.getElementById("stats");

editor.addEventListener("input", () => {
  const text  = editor.value;
  const chars = text.length;
  const words = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
  const mins  = Math.ceil(words / 200); // avg reading speed

  stats.textContent = `Words: ${words} | Characters: ${chars} | ~${mins} min read`;
});

Exercise 4: Color Picker Preview

Show a live preview box that changes background color to match a hex color input field.

What you learn: Input type="color", change events, inline style manipulation.

JavaScript
// HTML needed:
// <input type="color" id="colorPicker" value="#3b82f6">
// <input type="text" id="hexInput" value="#3b82f6">
// <div id="preview" style="width:100px;height:100px;"></div>

const picker   = document.getElementById("colorPicker");
const hexInput = document.getElementById("hexInput");
const preview  = document.getElementById("preview");

function applyColor(color) {
  preview.style.backgroundColor = color;
  picker.value   = color;
  hexInput.value = color;
}

picker.addEventListener("input", e => applyColor(e.target.value));
hexInput.addEventListener("change", e => {
  if (/^#[0-9a-f]{6}$/i.test(e.target.value)) applyColor(e.target.value);
});

applyColor("#3b82f6"); // initial
Beginner Tip: Every exercise above can be completed in a single HTML file using a <script> tag at the bottom of <body>. No build tools required. Open the file in a browser and iterate quickly.

Intermediate Exercises

These exercises introduce more complex DOM patterns, data persistence, and API integration.

Exercise 5: Todo List (No Frameworks)

A complete todo list with add, complete, delete, and filter β€” stored in an array and re-rendered on state change.

What you learn: State-driven rendering, array methods, event delegation, data-attributes.

JavaScript
// State
let todos = [];
let filter = "all"; // "all" | "active" | "completed"

// DOM refs
const input   = document.getElementById("todo-input");
const list    = document.getElementById("todo-list");
const filters = document.getElementById("filters");

// Add todo
document.getElementById("add-btn").addEventListener("click", () => {
  const text = input.value.trim();
  if (!text) return;
  todos.push({ id: Date.now(), text, done: false });
  input.value = "";
  render();
});

// Delegated events on list
list.addEventListener("click", e => {
  const id = Number(e.target.closest("[data-id]")?.dataset.id);
  if (!id) return;
  if (e.target.matches(".toggle"))  todos = todos.map(t => t.id === id ? {...t, done: !t.done} : t);
  if (e.target.matches(".delete"))  todos = todos.filter(t => t.id !== id);
  render();
});

// Filter buttons
filters.addEventListener("click", e => {
  if (e.target.matches("button")) { filter = e.target.dataset.filter; render(); }
});

function render() {
  const visible = todos.filter(t =>
    filter === "all" ? true : filter === "active" ? !t.done : t.done
  );
  list.innerHTML = visible.map(t => `
    <li data-id="${t.id}">
      <button class="toggle">${t.done ? "βœ…" : "⬜"}</button>
      <span style="text-decoration:${t.done ? "line-through" : "none"}">${t.text}</span>
      <button class="delete">πŸ—‘οΈ</button>
    </li>
  `).join("");
}

render();

Exercise 6: Simple Calculator

A four-operation calculator with a display, decimal support, and keyboard input handling.

What you learn: State machine thinking, expression evaluation, keyboard events.

JavaScript
let expression = "";
const display = document.getElementById("calc-display");

function updateDisplay(val) {
  display.textContent = val || "0";
}

function handleInput(value) {
  if (value === "=") {
    try {
      // Replace Γ— and Γ· if used
      const result = Function('"use strict"; return (' + expression + ')')();
      expression = String(result);
      updateDisplay(expression);
    } catch {
      updateDisplay("Error");
      expression = "";
    }
  } else if (value === "C") {
    expression = "";
    updateDisplay("0");
  } else if (value === "⌫") {
    expression = expression.slice(0, -1);
    updateDisplay(expression || "0");
  } else {
    expression += value;
    updateDisplay(expression);
  }
}

// Button clicks
document.getElementById("calc-buttons").addEventListener("click", e => {
  if (e.target.matches("button")) handleInput(e.target.dataset.value);
});

// Keyboard support
document.addEventListener("keydown", e => {
  const allowed = "0123456789+-*/.=";
  if (allowed.includes(e.key)) handleInput(e.key);
  if (e.key === "Enter")     handleInput("=");
  if (e.key === "Backspace") handleInput("⌫");
  if (e.key === "Escape")    handleInput("C");
});

Exercise 7: Fetch and Display API Data

Fetch a list of users from a public API and render them as cards with loading and error states.

What you learn: Fetch API, async/await, loading states, error handling, template rendering.

JavaScript
const container = document.getElementById("user-list");

async function loadUsers() {
  container.innerHTML = '<p class="loading">Loading…</p>';
  try {
    const res = await fetch("https://jsonplaceholder.typicode.com/users");
    if (!res.ok) throw new Error(`Status ${res.status}`);
    const users = await res.json();

    container.innerHTML = users.map(user => `
      <div class="user-card">
        <h3>${user.name}</h3>
        <p>πŸ“§ ${user.email}</p>
        <p>🏒 ${user.company.name}</p>
        <p>🌐 ${user.website}</p>
      </div>
    `).join("");
  } catch (err) {
    container.innerHTML = `<p class="error">Failed to load: ${err.message}</p>`;
  }
}

loadUsers();

Exercise 8: Form Validator

Validate a registration form (name, email, password) with real-time feedback and submission prevention.

What you learn: Form events, regex validation, CSS class toggling, preventing default submission.

JavaScript
const rules = {
  name:     { test: v => v.trim().length >= 2, msg: "Name must be at least 2 characters" },
  email:    { test: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), msg: "Enter a valid email" },
  password: { test: v => v.length >= 8 && /\d/.test(v), msg: "Min 8 chars, at least one number" },
};

function validateField(fieldName, value) {
  const rule    = rules[fieldName];
  const isValid = rule.test(value);
  const field   = document.getElementById(fieldName);
  const error   = document.getElementById(`${fieldName}-error`);
  field.classList.toggle("invalid", !isValid);
  field.classList.toggle("valid",   isValid);
  error.textContent = isValid ? "" : rule.msg;
  return isValid;
}

// Real-time validation
Object.keys(rules).forEach(name => {
  document.getElementById(name).addEventListener("input", e => {
    validateField(name, e.target.value);
  });
});

// On submit
document.getElementById("signup-form").addEventListener("submit", e => {
  e.preventDefault();
  const allValid = Object.keys(rules).every(name => {
    return validateField(name, document.getElementById(name).value);
  });
  if (allValid) alert("Form submitted successfully!");
});
Ad – 336Γ—280

Advanced Exercises

These exercises demonstrate patterns used in production JavaScript: debounce, pub/sub, localStorage persistence, and intersection observer for infinite scroll.

Exercise 9: Debounce Function Implementation

Implement debounce(fn, delay) from scratch and use it to optimize a search input that queries an API.

What you learn: Closures, higher-order functions, timers, performance optimization.

JavaScript
function debounce(fn, delay) {
  let timerId;
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// Usage: search input that waits 400ms after user stops typing
async function searchUsers(query) {
  if (!query) return;
  const res   = await fetch(`https://jsonplaceholder.typicode.com/users?q=${query}`);
  const users = await res.json();
  console.log(`Found ${users.length} users for "${query}"`);
}

const debouncedSearch = debounce(searchUsers, 400);

document.getElementById("search").addEventListener("input", e => {
  debouncedSearch(e.target.value);
  // Without debounce: fires on EVERY keystroke
  // With debounce:    fires only 400ms after last keystroke
});
Behavior: Typing "alice" rapidly fires only one API call 400ms after the final keystroke, instead of 5 calls for each character.

Exercise 10: Simple Pub/Sub Event System

Build a publish/subscribe (event bus) pattern that decouples components so they can communicate without direct references.

What you learn: Design patterns, closures, Map data structure, event-driven architecture.

JavaScript
function createEventBus() {
  const subscribers = new Map();

  return {
    subscribe(event, handler) {
      if (!subscribers.has(event)) subscribers.set(event, new Set());
      subscribers.get(event).add(handler);
      // Return unsubscribe function
      return () => subscribers.get(event).delete(handler);
    },
    publish(event, data) {
      if (!subscribers.has(event)) return;
      subscribers.get(event).forEach(handler => handler(data));
    },
    clear(event) {
      subscribers.delete(event);
    }
  };
}

// Usage
const bus = createEventBus();

// Component A subscribes
const unsub = bus.subscribe("user:login", data => {
  console.log("Header updated for:", data.name);
});

// Component B subscribes
bus.subscribe("user:login", data => {
  console.log("Analytics: login event for", data.email);
});

// Somewhere in your auth code
bus.publish("user:login", { name: "Alice", email: "alice@example.com" });
// Header updated for: Alice
// Analytics: login event for alice@example.com

unsub(); // Component A unsubscribes
bus.publish("user:login", { name: "Bob", email: "bob@example.com" });
// Analytics: login event for bob@example.com  (only Component B fires)
Output:
Header updated for: Alice
Analytics: login event for alice@example.com
Analytics: login event for bob@example.com

Exercise 11: localStorage Note-Taking App

A persistent note app that saves notes to localStorage β€” surviving page refreshes. Supports create, edit, and delete.

What you learn: localStorage CRUD, JSON serialization, state persistence, unique ID generation.

JavaScript
const STORAGE_KEY = "ylearner-notes";

// Load from localStorage or default to []
let notes = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");

function save() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
}

function addNote(text) {
  notes.push({ id: Date.now(), text, createdAt: new Date().toLocaleString() });
  save();
  render();
}

function deleteNote(id) {
  notes = notes.filter(n => n.id !== id);
  save();
  render();
}

function updateNote(id, text) {
  notes = notes.map(n => n.id === id ? { ...n, text } : n);
  save();
}

function render() {
  const container = document.getElementById("notes-container");
  container.innerHTML = notes.length === 0
    ? '<p class="empty">No notes yet. Add one above.</p>'
    : notes.map(n => `
      <div class="note" data-id="${n.id}">
        <textarea class="note-text">${n.text}</textarea>
        <div class="note-footer">
          <small>${n.createdAt}</small>
          <button class="delete-note">Delete</button>
        </div>
      </div>
    `).join("");
}

// Add note
document.getElementById("add-note-btn").addEventListener("click", () => {
  const textarea = document.getElementById("new-note");
  const text = textarea.value.trim();
  if (text) { addNote(text); textarea.value = ""; }
});

// Delete / edit via delegation
document.getElementById("notes-container").addEventListener("click", e => {
  if (e.target.matches(".delete-note")) {
    const id = Number(e.target.closest(".note").dataset.id);
    deleteNote(id);
  }
});
document.getElementById("notes-container").addEventListener("input", e => {
  if (e.target.matches(".note-text")) {
    const id = Number(e.target.closest(".note").dataset.id);
    updateNote(id, e.target.value);
  }
});

render(); // Initial render from stored data

Exercise 12: Infinite Scroll Simulation

Simulate infinite scroll by loading more items as the user approaches the bottom of the page, using the Intersection Observer API.

What you learn: IntersectionObserver API, async data loading patterns, DOM appending vs re-rendering, UX loading feedback.

JavaScript
let page  = 1;
let loading = false;
const ITEMS_PER_PAGE = 10;
const feed    = document.getElementById("feed");
const sentinel = document.getElementById("sentinel"); // empty div at bottom

// Simulated async data source
async function fetchItems(pageNum) {
  await new Promise(r => setTimeout(r, 600)); // simulate network delay
  return Array.from({ length: ITEMS_PER_PAGE }, (_, i) => ({
    id: (pageNum - 1) * ITEMS_PER_PAGE + i + 1,
    title: `Item #${(pageNum - 1) * ITEMS_PER_PAGE + i + 1}`,
    body: `Content loaded on page ${pageNum}.`
  }));
}

async function loadMore() {
  if (loading) return;
  loading = true;
  sentinel.textContent = "Loading…";

  const items = await fetchItems(page++);

  items.forEach(item => {
    const card = document.createElement("div");
    card.className = "feed-card";
    card.innerHTML = `<h3>${item.title}</h3><p>${item.body}</p>`;
    feed.insertBefore(card, sentinel);
  });

  sentinel.textContent = "";
  loading = false;
}

// Watch the sentinel element
const observer = new IntersectionObserver(entries => {
  if (entries[0].isIntersecting) loadMore();
}, { rootMargin: "200px" }); // start loading 200px before reaching bottom

observer.observe(sentinel);
loadMore(); // initial load
Behavior: The page loads 10 items. As the user scrolls down and the sentinel enters the viewport, 10 more items load automatically. The observer fires 200px early to pre-load before the user reaches the bottom.
IntersectionObserver vs scroll event: Avoid the scroll event for infinite scroll β€” it fires hundreds of times per second. IntersectionObserver is built for this use case: it runs off the main thread and calls your callback only when the element crosses the viewport boundary.
localStorage Limits: localStorage is synchronous and limited to ~5MB per origin. For larger data or complex queries, consider IndexedDB. Never store sensitive data (passwords, tokens) in localStorage.

Exercise Overview Table

ExerciseLevelKey ConceptsTime Estimate
Temperature ConverterBeginnerInput events, math15 min
CounterBeginnerState, DOM updates10 min
Word CounterBeginnerRegex, string methods15 min
Color PickerBeginnerInput types, styles20 min
Todo ListIntermediateState rendering, delegation45 min
CalculatorIntermediateState machine, keyboard60 min
Fetch UsersIntermediateFetch, async/await, errors30 min
Form ValidatorIntermediateRegex, feedback UX40 min
DebounceAdvancedClosures, HOF, timers25 min
Pub/SubAdvancedDesign patterns, Map35 min
Notes AppAdvancedlocalStorage, CRUD60 min
Infinite ScrollAdvancedIntersectionObserver45 min

Learning Path Recommendation

Complete exercises in order if you're new to JavaScript. If you're preparing for interviews specifically, prioritize:

  1. Debounce (closures + higher-order functions β€” extremely common interview question)
  2. Pub/Sub (design patterns + Map)
  3. Todo List (state-driven rendering)
  4. Form Validator (regex + real-world UX)

Extension Challenges

Once you've finished each exercise, enhance it:

  • Add a throttle function alongside debounce (Exercise 9)
  • Add "once" and "off" methods to the pub/sub bus (Exercise 10)
  • Add search/filter to the notes app (Exercise 11)
  • Add a "no more items" state to infinite scroll (Exercise 12)

FAQ

Do I need a framework to do these exercises?

No β€” all exercises use vanilla JavaScript. Learning the fundamentals without a framework makes you a much stronger developer when you later pick up React, Vue, or Svelte.

How do I test my exercises?

Open the HTML file in a browser and use the DevTools console. For automated testing, you can wrap pure functions in Jest or Vitest test suites β€” great practice for real-world development.

Can I use these exercises in my portfolio?

Absolutely. Polish one or two of the intermediate or advanced exercises, host them on GitHub Pages, and link them on your resume. Live demos are far more compelling than code alone.

What's the difference between debounce and the pub/sub pattern?

Debounce is a performance utility β€” it controls when a function fires. Pub/sub is an architectural pattern β€” it decouples components so they communicate without knowing about each other.

What should I build after these exercises?

Move on to full projects: a weather app, quiz app, or expense tracker. These combine multiple patterns and are more representative of real-world JavaScript work.

Summary

  • Beginner exercises: input events, DOM updates, string manipulation, basic state
  • Intermediate: state-driven rendering, fetch, event delegation, regex validation
  • Advanced: closures (debounce), design patterns (pub/sub), localStorage persistence, IntersectionObserver
  • Every exercise is self-contained and runnable in a browser with no build tools
  • Implementing debounce from memory is a real interview question at many companies
  • The pub/sub pattern demonstrates knowledge of design patterns beyond basic CRUD
  • Building a localStorage app shows understanding of persistence without a backend
  • IntersectionObserver knowledge signals awareness of performance best practices
  • State-driven rendering (update state β†’ re-render) is the mental model behind every major JS framework