Home Module 17 Coding Challenges
How to use this page: For each challenge, read the problem, close your laptop, and write your solution on paper or in a code editor. Only look at the solution once you have a working attempt. Struggling with the problem is where the learning happens.

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. i and x are 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.