If you’re serious about improving your JavaScript skills, reading alone isn’t enough — you need structured, real-world practice.
I’ve just published 30 additional JavaScript coding exercises (Exercises 21–50), each one including:
✅ Full, working code
✅ Clear learning objectives
✅ Step-by-step explanations
✅ Practical DOM, async, and browser API examples
This set goes beyond basics and helps you practice how JavaScript is actually used in modern web development:
🔹 Async / Await & Fetch APIs
🔹 DOM manipulation (tabs, modals, forms)
🔹 Search & filter logic
🔹 Timers, events, and state
🔹 Arrays, objects, algorithms, and UI patterns
Perfect for:
👩💻 Self-taught developers
👨🏫 Educators & instructors
📚 Students & bootcamp learners
🧠 Anyone building real JavaScript confidence
Exercises 21–35 (JavaScript Core)
21) FizzBuzz (Loops + Conditionals) — Console
Goal: Print numbers 1–100. For multiples of 3 print “Fizz”, multiples of 5 print “Buzz”, both → “FizzBuzz”.
for (let i = 1; i <= 100; i++) {
if (i % 15 === 0) {
console.log("FizzBuzz");
} else if (i % 3 === 0) {
console.log("Fizz");
} else if (i % 5 === 0) {
console.log("Buzz");
} else {
console.log(i);
}
}
Explanation
%checks divisibility via remainder.- Check
15first so numbers like30don’t get caught by the3check early. elseprints the number when no rule applies.
22) Palindrome Checker (Strings) — Console
Goal: Check if a word reads the same forward and backward.
function isPalindrome(word) {
const cleaned = word.toLowerCase().replace(/[^a-z0-9]/g, "");
const reversed = cleaned.split("").reverse().join("");
return cleaned === reversed;
}
console.log(isPalindrome("Racecar")); // true
console.log(isPalindrome("hello")); // false
console.log(isPalindrome("A man, a plan!"));// true-ish based on cleaning
Explanation
toLowerCase()makes comparison case-insensitive.replace(/[^a-z0-9]/g, "")removes non-alphanumeric characters.split("")→ array of chars,reverse(), thenjoin("")back to a string.
23) Factorial (Loops) — Console
Goal: Compute n! (e.g., 5! = 120).
function factorial(n) {
if (!Number.isInteger(n) || n < 0) return null;
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
console.log(factorial(5)); // 120
console.log(factorial(0)); // 1
console.log(factorial(-2)); // null
Explanation
- Validates input (must be a non-negative integer).
- Starts at
1and multiplies by every number up ton. 0!is defined as1, which this code naturally produces.
24) Fibonacci Sequence (Array) — Console
Goal: Generate the first n Fibonacci numbers.
function fibonacci(n) {
if (!Number.isInteger(n) || n <= 0) return [];
if (n === 1) return [0];
const seq = [0, 1];
while (seq.length < n) {
const next = seq[seq.length - 1] + seq[seq.length - 2];
seq.push(next);
}
return seq;
}
console.log(fibonacci(10));
Explanation
- Starts with
[0, 1]. - Each next value is the sum of the last two.
- Uses
whileuntil the array reaches lengthn.
25) Count Character Frequency (Objects) — Console
Goal: Count how many times each character appears in a string.
function charFrequency(text) {
const freq = {};
for (const ch of text) {
freq[ch] = (freq[ch] || 0) + 1;
}
return freq;
}
console.log(charFrequency("banana"));
Explanation
freqis an object where keys are characters.freq[ch] || 0defaults to 0 if the key doesn’t exist.- Adds 1 for each occurrence.
26) Sort Numbers Without .sort() (Algorithm) — Console
Goal: Sort an array ascending using Bubble Sort.
function bubbleSort(arr) {
const a = [...arr]; // copy so we don’t mutate input
for (let i = 0; i < a.length - 1; i++) {
for (let j = 0; j < a.length - 1 - i; j++) {
if (a[j] > a[j + 1]) {
[a[j], a[j + 1]] = [a[j + 1], a[j]]; // swap
}
}
}
return a;
}
console.log(bubbleSort([5, 3, 8, 1, 2]));
Explanation
- Outer loop controls passes.
- Inner loop compares adjacent pairs and swaps if out of order.
- ioptimization: after each pass, the largest value is already at the end.
27) Remove Duplicates Manually (No Set) — Console
Goal: Return unique values in order.
function uniqueValues(arr) {
const result = [];
for (const item of arr) {
if (!result.includes(item)) {
result.push(item);
}
}
return result;
}
console.log(uniqueValues([1, 2, 2, 3, 1, 4]));
Explanation
- Uses
result.includes(item)to check if already added. - Preserves first-seen order.
- Not the fastest for huge arrays, but perfect for learning.
28) Flatten One Level of Nested Arrays — Console
Goal: Turn [1,[2,3],[4],5] into [1,2,3,4,5].
function flattenOneLevel(arr) {
const out = [];
for (const item of arr) {
if (Array.isArray(item)) out.push(...item);
else out.push(item);
}
return out;
}
console.log(flattenOneLevel([1, [2, 3], [4], 5]));
Explanation
Array.isArraydetects nested arrays....itemspreads array elements intoout.- Only flattens one level (by design).
29) Group Items by Property — Console
Goal: Group objects by category.
function groupBy(arr, key) {
const grouped = {};
for (const obj of arr) {
const k = obj[key];
if (!grouped[k]) grouped[k] = [];
grouped[k].push(obj);
}
return grouped;
}
const products = [
{ name: "Apple", category: "Fruit" },
{ name: "Carrot", category: "Veg" },
{ name: "Banana", category: "Fruit" },
];
console.log(groupBy(products, "category"));
Explanation
- Uses an object of arrays.
- Each unique key (like
"Fruit") becomes a bucket. - Push each item into the correct bucket.
30) Safe JSON Parse (Try/Catch) — Console
Goal: Parse JSON without crashing your program.
function safeJsonParse(str) {
try {
return { ok: true, data: JSON.parse(str) };
} catch (err) {
return { ok: false, error: err.message };
}
}
console.log(safeJsonParse('{"a":1}'));
console.log(safeJsonParse("{bad json}"));
Explanation
JSON.parsethrows if the string isn’t valid JSON.try/catchprevents the error from stopping execution.- Returns a consistent result object.
31) Debounce (Functions + Timers) — Console/Browser
Goal: Only run a function after the user “stops” triggering it for X ms.
function debounce(fn, delayMs) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delayMs);
};
}
// Example:
const debouncedLog = debounce((msg) => console.log(msg), 500);
debouncedLog("A");
debouncedLog("B");
debouncedLog("C"); // only "C" logs after 500ms
Explanation
- A closure keeps
timerIdbetween calls. - Each call clears the previous timeout and sets a new one.
- Only the final call survives long enough to run.
32) Throttle (Functions + Timers) — Console/Browser
Goal: Run a function at most once every X ms.
function throttle(fn, intervalMs) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= intervalMs) {
lastTime = now;
fn(...args);
}
};
}
// Example:
const throttledLog = throttle(() => console.log("tick"), 1000);
setInterval(() => throttledLog(), 100); // logs about once per second
Explanation
lastTimetracks the last allowed execution time.- If not enough time passed, calls are ignored.
- Useful for scroll/resize events.
33) Build a URL Query String — Console
Goal: Convert an object to ?key=value&....
function toQueryString(params) {
const parts = [];
for (const key in params) {
const value = encodeURIComponent(params[key]);
parts.push(`${encodeURIComponent(key)}=${value}`);
}
return parts.length ? "?" + parts.join("&") : "";
}
console.log(toQueryString({ q: "js exercises", page: 2 }));
Explanation
encodeURIComponentmakes values safe for URLs.partsbecomes an array ofkey=valuepairs.- Joins them with
&and adds a leading?.
34) Deep Clone Simple JSON Objects — Console
Goal: Copy nested JSON-safe objects without shared references.
function deepCloneJson(obj) {
return JSON.parse(JSON.stringify(obj));
}
const original = { a: 1, b: { c: 2 } };
const clone = deepCloneJson(original);
clone.b.c = 999;
console.log(original.b.c); // 2
console.log(clone.b.c); // 999
Explanation
JSON.stringifyconverts to a string.JSON.parsecreates a brand new structure.- Limitation: won’t preserve functions, Dates, Maps, etc. (fine for learning + JSON data).
35) Validate Email (Basic Regex) — Console
Goal: Basic email format check.
function isEmail(str) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}
console.log(isEmail("test@example.com")); // true
console.log(isEmail("bad@email")); // false
Explanation
- This regex checks:
- something before
@ - something after
@ - at least one dot section
- something before
- Not “perfect” for every valid email ever, but solid for beginner validation.
Exercises 36–50 (DOM + Practical Web Skills)
For these, use this HTML template once, then swap scripts per exercise:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>JS Exercises</title>
<style>
body { font-family: Arial, sans-serif; padding: 16px; }
.hidden { display: none; }
.error { color: #b00020; }
.ok { color: #0a7a0a; }
.box { padding: 12px; border: 1px solid #ddd; border-radius: 8px; margin: 12px 0; }
</style>
</head>
<body>
<div id="app"></div>
<script src="script.js"></script>
</body>
</html>
36) Live Character Counter — DOM
Goal: Count characters as the user types.
script.js
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Live Character Counter</h2>
<textarea id="txt" rows="4" cols="40" placeholder="Type here..."></textarea>
<p>Characters: <span id="count">0</span></p>
</div>
`;
const txt = document.getElementById("txt");
const count = document.getElementById("count");
txt.addEventListener("input", () => {
count.textContent = txt.value.length;
});
Explanation
inputevent fires on every change (typing, paste, delete).txt.value.lengthgives current character count.- Updates the DOM span instantly.
37) To-Do List Add Items — DOM
Goal: Add items to a list from an input field.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>To-Do List</h2>
<input id="taskInput" placeholder="New task" />
<button id="addBtn">Add</button>
<ul id="list"></ul>
</div>
`;
const input = document.getElementById("taskInput");
const addBtn = document.getElementById("addBtn");
const list = document.getElementById("list");
addBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
const li = document.createElement("li");
li.textContent = text;
list.appendChild(li);
input.value = "";
input.focus();
});
Explanation
trim()prevents empty/space-only tasks.createElement("li")creates new list items.- Resetting and focusing improves user experience.
38) To-Do Remove Items (Event Delegation) — DOM
Goal: Click a list item to remove it.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Click-to-Remove List</h2>
<input id="taskInput" placeholder="New item" />
<button id="addBtn">Add</button>
<ul id="list"></ul>
<p><em>Tip: click an item to remove it.</em></p>
</div>
`;
const input = document.getElementById("taskInput");
const addBtn = document.getElementById("addBtn");
const list = document.getElementById("list");
addBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
const li = document.createElement("li");
li.textContent = text;
list.appendChild(li);
input.value = "";
});
// Event delegation: one listener on the parent <ul>
list.addEventListener("click", (e) => {
if (e.target.tagName === "LI") {
e.target.remove();
}
});
Explanation
- Instead of adding a click handler to every
<li>, we add one to the<ul>. e.targetis the clicked element..remove()deletes it from the DOM.
39) Tabs UI (Switch Sections) — DOM
Goal: Click buttons to show different content panels.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Tabs</h2>
<button data-tab="one">Tab 1</button>
<button data-tab="two">Tab 2</button>
<button data-tab="three">Tab 3</button>
<div id="one" class="panel">Content for Tab 1</div>
<div id="two" class="panel hidden">Content for Tab 2</div>
<div id="three" class="panel hidden">Content for Tab 3</div>
</div>
`;
function showTab(id) {
document.querySelectorAll(".panel").forEach(p => p.classList.add("hidden"));
document.getElementById(id).classList.remove("hidden");
}
document.querySelectorAll("button[data-tab]").forEach(btn => {
btn.addEventListener("click", () => showTab(btn.dataset.tab));
});
Explanation
- Panels share class
panel; we hide all, then reveal the chosen one. dataset.tabreadsdata-tab="...".- This pattern scales to many tabs.
40) Modal Popup (Open/Close) — DOM
Goal: Open a modal overlay and close it.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Modal</h2>
<button id="open">Open Modal</button>
</div>
<div id="overlay" class="hidden" style="
position:fixed; inset:0; background:rgba(0,0,0,.5);
display:flex; align-items:center; justify-content:center;">
<div style="background:#fff; padding:16px; border-radius:10px; width:300px;">
<h3>Modal Title</h3>
<p>This is a simple modal.</p>
<button id="close">Close</button>
</div>
</div>
`;
const overlay = document.getElementById("overlay");
document.getElementById("open").addEventListener("click", () => {
overlay.classList.remove("hidden");
});
document.getElementById("close").addEventListener("click", () => {
overlay.classList.add("hidden");
});
// Close if user clicks outside the modal box
overlay.addEventListener("click", (e) => {
if (e.target === overlay) overlay.classList.add("hidden");
});
Explanation
- Overlay covers the screen and centers the modal content.
- Clicking the overlay (not the inner box) closes it.
- Uses
.hiddento control visibility.
41) Form Validation with Inline Errors — DOM
Goal: Validate name + email and show errors.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Signup</h2>
<form id="form">
<div>
<label>Name</label><br/>
<input id="name" />
<div id="nameErr" class="error"></div>
</div>
<div style="margin-top:8px;">
<label>Email</label><br/>
<input id="email" />
<div id="emailErr" class="error"></div>
</div>
<button style="margin-top:10px;">Submit</button>
<p id="status"></p>
</form>
</div>
`;
const form = document.getElementById("form");
const nameEl = document.getElementById("name");
const emailEl = document.getElementById("email");
const nameErr = document.getElementById("nameErr");
const emailErr = document.getElementById("emailErr");
const status = document.getElementById("status");
function isEmail(str) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}
form.addEventListener("submit", (e) => {
e.preventDefault();
nameErr.textContent = "";
emailErr.textContent = "";
status.textContent = "";
const name = nameEl.value.trim();
const email = emailEl.value.trim();
let ok = true;
if (name.length < 2) {
nameErr.textContent = "Name must be at least 2 characters.";
ok = false;
}
if (!isEmail(email)) {
emailErr.textContent = "Please enter a valid email address.";
ok = false;
}
status.textContent = ok ? "✅ Submitted (demo)!" : "❌ Fix errors above.";
status.className = ok ? "ok" : "error";
});
Explanation
- Prevents page reload via
preventDefault(). - Clears old errors each submit.
- Uses
okflag to track validation results. - Provides immediate, targeted feedback.
42) Theme Toggle (Dark/Light) — DOM
Goal: Switch page theme with a button.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Theme Toggle</h2>
<button id="toggle">Toggle Theme</button>
<p>Click the button to switch styles.</p>
</div>
`;
let dark = false;
document.getElementById("toggle").addEventListener("click", () => {
dark = !dark;
document.body.style.background = dark ? "#111" : "#fff";
document.body.style.color = dark ? "#fff" : "#111";
});
Explanation
- Tracks state with
dark. - Updates inline styles on the body.
- Simple pattern that can later be upgraded to CSS classes.
43) Random Quote Generator (Array + DOM) — DOM
Goal: Show a random quote each click.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Random Quote</h2>
<button id="new">New Quote</button>
<p id="quote" style="margin-top:10px;"></p>
</div>
`;
const quotes = [
"Small steps, daily.",
"Make it work, then make it better.",
"Practice beats theory.",
"Debugging is learning in disguise."
];
const quoteEl = document.getElementById("quote");
function showRandomQuote() {
const idx = Math.floor(Math.random() * quotes.length);
quoteEl.textContent = quotes[idx];
}
document.getElementById("new").addEventListener("click", showRandomQuote);
showRandomQuote();
Explanation
- Random index:
Math.floor(Math.random() * length). - Updates quote text.
- Calls once at start so the page isn’t empty.
44) Simple Stopwatch (Start/Stop/Reset) — DOM
Goal: Build a stopwatch with interval control.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Stopwatch</h2>
<h3 id="time">0.0</h3>
<button id="start">Start</button>
<button id="stop">Stop</button>
<button id="reset">Reset</button>
</div>
`;
const timeEl = document.getElementById("time");
let t = 0; // tenths of a second
let timerId = null;
function render() {
timeEl.textContent = (t / 10).toFixed(1);
}
document.getElementById("start").addEventListener("click", () => {
if (timerId !== null) return; // already running
timerId = setInterval(() => {
t++;
render();
}, 100);
});
document.getElementById("stop").addEventListener("click", () => {
clearInterval(timerId);
timerId = null;
});
document.getElementById("reset").addEventListener("click", () => {
t = 0;
render();
});
render();
Explanation
- Uses tenths of seconds (
100ms) for a smoother display. - Stores interval id in
timerIdso it can be stopped. - Prevents multiple intervals with the
if (timerId !== null)guard.
45) Countdown Timer (Input + Interval) — DOM
Goal: User enters seconds; countdown to 0.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Countdown</h2>
<input id="secs" type="number" min="1" placeholder="Seconds" />
<button id="go">Start</button>
<p id="out"></p>
</div>
`;
const secsInput = document.getElementById("secs");
const out = document.getElementById("out");
let id = null;
document.getElementById("go").addEventListener("click", () => {
let remaining = Number(secsInput.value);
if (!Number.isFinite(remaining) || remaining <= 0) {
out.textContent = "Enter a positive number.";
return;
}
clearInterval(id);
out.textContent = `Remaining: ${remaining}s`;
id = setInterval(() => {
remaining--;
out.textContent = `Remaining: ${remaining}s`;
if (remaining <= 0) {
clearInterval(id);
out.textContent = "✅ Done!";
}
}, 1000);
});
Explanation
- Validates input and converts to number.
- Clears any existing countdown to avoid multiple timers.
- Decrements once per second until 0, then stops interval.
46) LocalStorage Notes App — DOM + Storage
Goal: Save notes in the browser.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Notes (Saved in Browser)</h2>
<textarea id="note" rows="5" cols="40" placeholder="Write notes..."></textarea>
<div>
<button id="save">Save</button>
<button id="clear">Clear</button>
</div>
<p id="msg"></p>
</div>
`;
const note = document.getElementById("note");
const msg = document.getElementById("msg");
note.value = localStorage.getItem("notes") || "";
document.getElementById("save").addEventListener("click", () => {
localStorage.setItem("notes", note.value);
msg.textContent = "✅ Saved!";
msg.className = "ok";
});
document.getElementById("clear").addEventListener("click", () => {
note.value = "";
localStorage.removeItem("notes");
msg.textContent = "🗑️ Cleared!";
msg.className = "";
});
Explanation
- Loads saved notes on startup.
- Saves text to
localStorageunder a key ("notes"). - Clear removes both UI text and stored data.
47) Simple Search Filter (List Filtering) — DOM
Goal: Filter visible items based on a search box.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Search Filter</h2>
<input id="search" placeholder="Search..." />
<ul id="items"></ul>
</div>
`;
const data = ["JavaScript", "HTML", "CSS", "DOM", "Events", "Arrays", "Objects"];
const ul = document.getElementById("items");
const search = document.getElementById("search");
function render(list) {
ul.innerHTML = "";
for (const item of list) {
const li = document.createElement("li");
li.textContent = item;
ul.appendChild(li);
}
}
search.addEventListener("input", () => {
const q = search.value.toLowerCase().trim();
const filtered = data.filter(x => x.toLowerCase().includes(q));
render(filtered);
});
render(data);
Explanation
- Renders from an array to the DOM.
- On each input:
- Normalize query to lowercase.
- Filter items by
includes. - Re-render the list with matching results.
48) Keyboard Shortcut (Ctrl/Cmd + K) — DOM
Goal: Open a “search” UI using keyboard shortcuts.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Keyboard Shortcut</h2>
<p>Press <strong>Ctrl+K</strong> (Windows) or <strong>Cmd+K</strong> (Mac).</p>
<input id="search" class="hidden" placeholder="Type to search..." />
</div>
`;
const search = document.getElementById("search");
document.addEventListener("keydown", (e) => {
const isMac = navigator.platform.toLowerCase().includes("mac");
const combo = (isMac && e.metaKey && e.key.toLowerCase() === "k")
|| (!isMac && e.ctrlKey && e.key.toLowerCase() === "k");
if (combo) {
e.preventDefault();
search.classList.remove("hidden");
search.focus();
}
if (e.key === "Escape") {
search.classList.add("hidden");
search.value = "";
}
});
Explanation
- Listens globally on
document. - Detects Ctrl+K or Cmd+K.
preventDefault()stops browser’s default search behavior.- Escape hides and clears the input.
49) Fetch JSON (Async/Await) — Browser
Goal: Load data from an API and display it.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Fetch Demo</h2>
<button id="load">Load Data</button>
<pre id="out" style="white-space:pre-wrap;"></pre>
</div>
`;
const out = document.getElementById("out");
document.getElementById("load").addEventListener("click", async () => {
out.textContent = "Loading...";
try {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
} catch (err) {
out.textContent = "Error: " + err.message;
}
});
Explanation
fetch(url)returns a Promise for the response.await res.json()parses response body as JSON.try/catchhandles network errors and failed status codes.- Displays formatted JSON using
JSON.stringify(..., null, 2).
50) Simple Pagination (Slice + Render) — DOM
Goal: Show items 5 at a time with Next/Prev.
document.getElementById("app").innerHTML = `
<div class="box">
<h2>Pagination</h2>
<ul id="list"></ul>
<button id="prev">Prev</button>
<button id="next">Next</button>
<p id="info"></p>
</div>
`;
const items = Array.from({ length: 23 }, (_, i) => `Item ${i + 1}`);
const pageSize = 5;
let page = 0;
const list = document.getElementById("list");
const info = document.getElementById("info");
function render() {
list.innerHTML = "";
const start = page * pageSize;
const pageItems = items.slice(start, start + pageSize);
for (const it of pageItems) {
const li = document.createElement("li");
li.textContent = it;
list.appendChild(li);
}
const totalPages = Math.ceil(items.length / pageSize);
info.textContent = `Page ${page + 1} of ${totalPages}`;
}
document.getElementById("prev").addEventListener("click", () => {
page = Math.max(0, page - 1);
render();
});
document.getElementById("next").addEventListener("click", () => {
const totalPages = Math.ceil(items.length / pageSize);
page = Math.min(totalPages - 1, page + 1);
render();
});
render();
Explanation
slice(start, end)grabs only items for the current page.page * pageSizecomputes where the page starts.Math.ceilcomputes total pages.- Prev/Next clamp the page so it doesn’t go out of range.