Skip to content
JavaScript interview 4 min read

JavaScript Coding Challenges

“Implement this from scratch” questions are an interview staple because they reveal whether you understand the building blocks — closures, recursion, promises, and the iteration protocols — rather than just the library that wraps them. Each challenge below comes with a clean, runnable solution and a short note on the idea that makes it work. Type them out, predict the output, and make sure you can rebuild each one without looking.

Implement debounce

Debounce delays a function until activity stops, so rapid calls collapse into a single one — perfect for search-as-you-type. The trick is a closure holding a timer that every call resets.

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

const log = debounce((q) => console.log("search:", q), 300);
log("a"); log("ab"); log("abc"); // only "search: abc" fires, once

Implement throttle

Throttle guarantees at most one call per interval — ideal for scroll and resize handlers. A boolean gate blocks calls until the cooldown elapses.

function throttle(fn, limit) {
  let waiting = false;
  return function (...args) {
    if (waiting) return;
    fn.apply(this, args);
    waiting = true;
    setTimeout(() => (waiting = false), limit);
  };
}
TechniqueFiresUse case
DebounceAfter the pauseAutocomplete, validation
ThrottleAt a steady rateScroll, drag, resize

Deep clone an object

A shallow copy shares nested references; a deep clone duplicates everything. Modern engines ship structuredClone, but interviewers usually want the recursive version that shows you understand the structure.

function deepClone(value) {
  if (value === null || typeof value !== "object") return value;
  if (value instanceof Date) return new Date(value);
  if (Array.isArray(value)) return value.map(deepClone);
  return Object.fromEntries(
    Object.entries(value).map(([k, v]) => [k, deepClone(v)]),
  );
}

const original = { user: { name: "Ada" }, tags: [1, 2] };
const copy = deepClone(original);
copy.user.name = "Linus";
console.log(original.user.name);

Output:

Ada

structuredClone(obj) handles cycles, Maps, and Sets natively — reach for it in real code and reserve the hand-rolled version for interviews.

Flatten a nested array

Array.prototype.flat(Infinity) does this in one call, but the recursive reduce version is the canonical interview answer.

function flatten(arr) {
  return arr.reduce(
    (acc, item) =>
      acc.concat(Array.isArray(item) ? flatten(item) : item),
    [],
  );
}

console.log(flatten([1, [2, [3, [4]], 5]]));

Output:

[ 1, 2, 3, 4, 5 ]

Implement curry

Currying collects arguments until there are enough to call the original function. Compare the count of received args against fn.length (its declared arity).

function curry(fn) {
  return function curried(...args) {
    return args.length >= fn.length
      ? fn.apply(this, args)
      : (...rest) => curried.apply(this, [...args, ...rest]);
  };
}

const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3), add(1, 2)(3), add(1, 2, 3));

Output:

6 6 6

Implement memoize

Memoization caches results by argument key so a pure function never recomputes the same answer.

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;
  };
}

const slowSquare = memoize((n) => n * n);
console.log(slowSquare(8), slowSquare(8)); // second call is cached

Build a Promise from scratch

Reimplementing the core of a promise proves you understand the resolve/reject state machine and that then returns a new promise.

class MyPromise {
  constructor(executor) {
    this.callbacks = [];
    this.state = "pending";
    const resolve = (value) => {
      if (this.state !== "pending") return;
      this.state = "fulfilled";
      this.value = value;
      this.callbacks.forEach((cb) => cb(value));
    };
    executor(resolve, () => {});
  }
  then(onFulfilled) {
    if (this.state === "fulfilled") onFulfilled(this.value);
    else this.callbacks.push(onFulfilled);
    return this;
  }
}

new MyPromise((resolve) => setTimeout(() => resolve(42), 0))
  .then((v) => console.log("resolved:", v));

Implement Promise.all

Promise.all resolves with an ordered array once every input settles, and rejects as soon as any one rejects.

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let remaining = promises.length;
    if (remaining === 0) return resolve(results);
    promises.forEach((p, i) => {
      Promise.resolve(p).then((value) => {
        results[i] = value;
        if (--remaining === 0) resolve(results);
      }, reject);
    });
  });
}

promiseAll([Promise.resolve(1), Promise.resolve(2), 3]).then(console.log);

Output:

[ 1, 2, 3 ]

Group an array by key

group-by turns a flat list into buckets — a reduce that pushes each item into an array keyed by some derived value.

function groupBy(arr, keyFn) {
  return arr.reduce((acc, item) => {
    const key = keyFn(item);
    (acc[key] ??= []).push(item);
    return acc;
  }, {});
}

const people = [
  { name: "Ada", role: "dev" },
  { name: "Grace", role: "dev" },
  { name: "Alan", role: "ops" },
];
console.log(groupBy(people, (p) => p.role));

Output:

{ dev: [ { name: 'Ada', role: 'dev' }, { name: 'Grace', role: 'dev' } ],
  ops: [ { name: 'Alan', role: 'ops' } ] }

Best Practices

  • Reach for built-ins (flat, structuredClone, Object.groupBy) in production; rebuild them only to demonstrate understanding.
  • Lean on closures for any utility that needs private state across calls (debounce, throttle, memoize).
  • Use Promise.resolve(value) to normalize inputs so your combinators accept both promises and plain values.
  • Pick a stable cache key strategy for memoization; JSON.stringify is fine for primitives but breaks on functions and circular data.
  • Always handle the empty-input edge case — an empty array should resolve Promise.all immediately, not hang.
  • Talk through your approach before coding; interviewers reward a clear plan over a silent scramble.
Last updated June 1, 2026
Was this helpful?