Practical Coding Challenges
10 coding challenges typical of frontend interviews — solve each before reading the solution.
JavaScript Challenges
Challenge 1 — Flatten an array
Write a function that flattens a nested array to any depth.
flatten([1, [2, [3, [4]], 5]])
// → [1, 2, 3, 4, 5]
Show Solution
// Modern (ES2019+)
function flatten(arr) {
return arr.flat(Infinity);
}
// Manual recursive solution
function flatten(arr) {
return arr.reduce((acc, item) =>
acc.concat(Array.isArray(item) ? flatten(item) : item), []);
}
Challenge 2 — Debounce
Implement a debounce(fn, delay) function that delays fn until delay ms have passed since the last call.
const debounced = debounce(search, 300);
input.addEventListener('input', debounced);
Show Solution
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
Challenge 3 — Group array of objects by key
Write a function that groups an array of objects by a given key.
const people = [
{ name: 'Alice', dept: 'Engineering' },
{ name: 'Bob', dept: 'Design' },
{ name: 'Carol', dept: 'Engineering' }
];
groupBy(people, 'dept');
// → { Engineering: [{...}, {...}], Design: [{...}] }
Show Solution
function groupBy(arr, key) {
return arr.reduce((acc, item) => {
const group = item[key];
acc[group] = acc[group] ?? [];
acc[group].push(item);
return acc;
}, {});
}
// Or using Object.groupBy (ES2024):
Object.groupBy(people, p => p.dept);
Challenge 4 — Deep clone an object
Write a function that creates a deep copy of an object (without using JSON.parse/stringify — handle arrays and nested objects).
Show Solution
function deepClone(value) {
if (value === null || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(deepClone);
return Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, deepClone(v)])
);
}
// Modern native alternative:
structuredClone(value);
Challenge 5 — Memoise a function
Implement a memoize(fn) function that caches results of expensive function calls.
const slowDouble = n => { /* ...slow... */ return n * 2; };
const fastDouble = memoize(slowDouble);
fastDouble(5); // computes
fastDouble(5); // returns cached result
Show Solution
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;
};
}
DOM Challenges
Challenge 6 — Build a star rating component
Given a container element, render 5 star buttons. Clicking star N fills stars 1 through N. Hovering previews the rating. Display the selected rating number.
Show Solution
function createStarRating(container) {
let selected = 0;
const stars = [];
for (let i = 1; i <= 5; i++) {
const btn = document.createElement('button');
btn.textContent = '☆';
btn.dataset.value = i;
btn.style.cssText = 'font-size:2rem;background:none;border:none;cursor:pointer';
btn.addEventListener('mouseenter', () => paint(i));
btn.addEventListener('mouseleave', () => paint(selected));
btn.addEventListener('click', () => { selected = i; paint(i); });
container.appendChild(btn);
stars.push(btn);
}
function paint(n) {
stars.forEach((s, idx) => {
s.textContent = idx < n ? '★' : '☆';
s.style.color = idx < n ? '#f59e0b' : '#94a3b8';
});
}
}
Challenge 7 — Implement event delegation for a dynamic list
Given an unordered list where items can be dynamically added, attach a single event listener that handles clicking "delete" buttons inside list items without re-attaching listeners when new items are added.
Show Solution
const list = document.querySelector('#list');
// One listener on the parent
list.addEventListener('click', e => {
const delBtn = e.target.closest('[data-action="delete"]');
if (!delBtn) return;
delBtn.closest('li').remove();
});
function addItem(text) {
const li = document.createElement('li');
const span = document.createElement('span');
span.textContent = text;
const btn = document.createElement('button');
btn.dataset.action = 'delete';
btn.textContent = 'Delete';
li.append(span, btn);
list.appendChild(li);
}
Challenge 8 — Infinite scroll
Load more items when the user scrolls to within 200px of the bottom of the page. Use IntersectionObserver or scroll events. Prevent duplicate loads while a request is in progress.
Show Solution
let page = 1;
let loading = false;
const sentinel = document.querySelector('#sentinel'); // a div at the bottom
const observer = new IntersectionObserver(async entries => {
if (!entries[0].isIntersecting || loading) return;
loading = true;
const items = await fetchPage(page++);
renderItems(items);
loading = false;
}, { rootMargin: '200px' });
observer.observe(sentinel);
Async Challenges
Challenge 9 — Fetch with retry
Write an fetchWithRetry(url, retries) function that retries the request up to retries times on failure, with a 1-second delay between attempts.
Show Solution
async function fetchWithRetry(url, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (attempt === retries) throw err;
await new Promise(r => setTimeout(r, 1000)); // wait 1s
console.log(`Retrying (${attempt}/${retries})...`);
}
}
}
Challenge 10 — Sequential vs parallel fetching
You have an array of user IDs. Fetch all their profiles. Show two implementations: one that fetches them sequentially and one that fetches them in parallel. What is the performance difference?
Show Solution
const ids = [1, 2, 3, 4, 5];
// Sequential — each waits for the previous
// Total time = sum of all request times
async function sequential() {
const users = [];
for (const id of ids) {
const res = await fetch(`/users/${id}`);
users.push(await res.json());
}
return users;
}
// Parallel — all start at once
// Total time = slowest single request
async function parallel() {
const promises = ids.map(id =>
fetch(`/users/${id}`).then(r => r.json())
);
return Promise.all(promises);
}
// Parallel is dramatically faster — use it whenever
// requests are independent of each other.
Interview Tips for Coding Challenges
- Think aloud. Interviewers want to hear your thought process — explaining as you code is often more important than the final answer.
- Clarify before writing. Ask about edge cases, input format, and constraints. This shows professional instincts.
- Write a brute force first. Get something working. Then discuss how to optimise.
- Test with examples. Mentally trace through your code with the sample input before submitting.
- Name variables clearly.
iandxare fine in loops, but prefer descriptive names in the rest of your code. - Handle edge cases. Empty input, single element, very large input — mentioning these even if you don't code them shows maturity.
- Know your time complexity. Be ready to describe your solution as O(n), O(n²), etc. if asked.