Lesson overview · Free interview practice

Higher-Order Functions

Master functions as first-class citizens and essential array methods.

Topics in the full lesson
  • Functions as First-Class Citizens
  • Higher-Order Functions
  • Callbacks
  • Array Methods: Map, Filter, Reduce
  • Best Practices
  • Exercises

Practice Problems

Work through a question before revealing its explanation. These questions and answers are free; Premium adds the full lesson walkthrough, examples and implementation detail.

Completion marks record your own progress, not an automatically checked result. The task type does not determine whether it is optional.

TheoryEasy

What is a Higher-Order Function

Question

What is a higher-order function (HOF)? Show both forms with code, and explain why HOFs are common in JavaScript.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryEasy

First-Class Functions

Question

What does "first-class functions" mean in JavaScript? Show the four capabilities that follow from it.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryEasy

`map` vs `forEach`

Question

How does map differ from forEach? When should you reach for each?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryMedium

How `reduce` Works

Question

Explain how reduce works. Show the canonical patterns: reduce-to-value and reduce-to-structure.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

CodingHard

Implement `debounce`

Implement and check your solution in the stated environment before revealing the answer.

Question

Implement debounce(fn, wait). It should delay fn until a quiet period follows the latest call. Walk through how the returned function uses its closure.

Optional advanced extension. First finish a small returned-function exercise such as makeAdder. For this task, review closures, timers and the difference between a normal function's this and an arrow's lexical this.

Contract

  • Implement trailing-only debounce. Each call replaces any pending timer with a new one. No callback runs immediately.
  • fn must be callable or creation throws TypeError. wait must be a number that is an integer from 1 through 2,147,483,647 milliseconds, otherwise creation throws RangeError. There is no coercion. Zero is deliberately excluded from this timer exercise.
  • When the surviving timer executes, call fn once with the latest argument list and receiver. A later empty call replaces earlier arguments with an empty list.
  • The wrapper always returns undefined. It does not return the callback result or a promise.
  • Clear the pending-state marker before invoking fn. A callback can schedule a fresh debounced call. A thrown callback error escapes the timer callback. It is not synchronously thrown by the earlier call that scheduled it.
  • Calls and timer callbacks run in event-loop order. If a new call runs before the old timer callback, it cancels that pending call even at its nominal deadline. If the timer has already run, a new call starts another quiet period.
  • Real timers may run late. The contract does not promise exact wall-clock execution times. No leading option, maximum wait, cancel or flush method is required.

With calls at 0 and 40 milliseconds and wait = 100, an ideal controlled clock should show one callback at 140 with the second call's data. Continued activity can postpone the callback indefinitely.

The clock fixture below is test infrastructure, not another implementation exercise. It replaces only the two global timer functions while each synchronous check runs and restores them in finally.

Set up an attempt

Use Node.js 24.21.0. Work in a small local folder, save the files below exactly as named, then edit only the starter. The .mjs extension makes the JavaScript files ES modules. No package install, browser, network request or project framework is needed.

Run the visible checks from that folder:

node check.mjs

The supplied starter intentionally throws an Implement ... error. That first failure confirms that the checker reaches your code. Work toward the named success message at the end of the check, not toward hiding assertions.

These are finite checks of the stated cases, not a proof of all possible correctness or mastery. Be ready to explain the behavior as well as run it.

starter.mjs

export function debounce(fn, wait) {
  throw new Error('Implement debounce')
}

clock.mjs

import assert from 'node:assert/strict'

export function withClock(check) {
  const originalSet = globalThis.setTimeout
  const originalClear = globalThis.clearTimeout
  const jobs = new Map()
  let now = 0
  let sequence = 0

  globalThis.setTimeout = (callback, delay) => {
    const id = ++sequence
    jobs.set(id, { callback, due: now + delay })
    return id
  }
  globalThis.clearTimeout = (id) => jobs.delete(id)

  const clock = {
    get now() { return now },
    get pending() { return jobs.size },
    elapse(ms) { now += ms },
    advance(ms) {
      const target = now + ms
      while (true) {
        const next = [...jobs]
          .filter(([, job]) => job.due <= target)
          .sort((a, b) => a[1].due - b[1].due || a[0] - b[0])[0]
        if (!next) break
        const [id, job] = next
        now = Math.max(now, job.due)
        jobs.delete(id)
        job.callback()
      }
      now = target
    },
  }

  try {
    check(clock)
    assert.equal(clock.pending, 0, 'A check left timers pending')
  } finally {
    globalThis.setTimeout = originalSet
    globalThis.clearTimeout = originalClear
  }
}

check.mjs

import assert from 'node:assert/strict'
import { withClock } from './clock.mjs'
const { debounce } = await import(new URL(process.argv[2] ?? './starter.mjs', import.meta.url))

withClock((clock) => {
  const calls = []
  const a = { label: 'a' }
  const b = { label: 'b' }
  const run = debounce(function (...args) {
    calls.push([clock.now, this, args])
    return 42
  }, 100)
  assert.equal(run.call(a, 'old'), undefined)
  clock.advance(40)
  run.call(b, 'new', 2)
  assert.equal(clock.pending, 1)
  clock.advance(99)
  assert.deepEqual(calls, [])
  clock.advance(1)
  assert.deepEqual(calls, [[140, b, ['new', 2]]])
  run()
  clock.advance(100)
  assert.deepEqual(calls[1], [240, undefined, []])
  clock.advance(1000)
  assert.equal(calls.length, 2)
})

withClock((clock) => {
  const seen = []
  const run = debounce((value) => seen.push([clock.now, value]), 10)
  run('old')
  clock.elapse(10)
  run('new')
  clock.advance(0)
  assert.deepEqual(seen, [])
  clock.advance(10)
  assert.deepEqual(seen, [[20, 'new']])
  run('first')
  clock.advance(10)
  run('second')
  clock.advance(10)
  assert.deepEqual(seen.slice(1), [[30, 'first'], [40, 'second']])
})

withClock((clock) => {
  const marker = new Error('callback failed')
  let count = 0
  const run = debounce(() => {
    count += 1
    if (count === 1) throw marker
  }, 5)
  run()
  assert.throws(() => clock.advance(5), (error) => error === marker)
  run()
  clock.advance(5)
  assert.equal(count, 2)
  const seen = []
  const recursive = debounce((n) => {
    seen.push(n)
    if (n === 1) recursive(2)
  }, 5)
  recursive(1)
  clock.advance(5)
  assert.deepEqual(seen, [1])
  clock.advance(5)
  assert.deepEqual(seen, [1, 2])
})

withClock((clock) => {
  let left = 0
  let right = 0
  const a = debounce(() => left += 1, 1)
  const b = debounce(() => right += 1, 1)
  a()
  b()
  a()
  clock.advance(1)
  assert.deepEqual([left, right], [1, 1])
  const max = debounce(() => left += 1, 2_147_483_647)
  max()
  clock.advance(2_147_483_647)
  assert.equal(left, 2)
})
for (const wait of [0, -1, 1.5, NaN, Infinity, '10', 2_147_483_648]) {
  assert.throws(() => debounce(() => {}, wait), RangeError)
}
assert.throws(() => debounce(null, 10), TypeError)
console.log('debounce checks passed')

Optional TypeScript attempt

Use this instead of the JavaScript starter if you want a typed implementation. Save starter.mts, types.mts and tsconfig.json below beside the same check.mjs. The @ts-expect-error lines are intentional negative type checks. Their guarded block is not executed.

With an existing TypeScript 5.9.3 compiler available, run both commands:

tsc -p tsconfig.json
node check.mjs ./starter.mts

The first command performs a strict no-emit type check. The second runs the behavioral checks using Node's type stripping. Stripping is not type checking. The unimplemented typed starter should type-check but fail the runtime check until you implement it.

The configuration uses standard library declarations only. No @types/node package is needed for these small fixtures. The timer examples use the shared timer API from the DOM declarations, not browser execution.

starter.mts

export function debounce<This, Args extends unknown[]>(
  fn: (this: This, ...args: Args) => unknown,
  wait: number,
): (this: This, ...args: Args) => void {
  throw new Error('Implement debounce')
}

types.mts

import { debounce } from './starter.mts'

type Context = { label: string }
const wrapped = debounce(function (this: Context, value: number, suffix: string) {
  return this.label + value + suffix
}, 10)
const context: Context = { label: 'value: ' }
const result: void = wrapped.call(context, 1, '!')
if (false) {
  // @ts-expect-error The callback needs a number first.
  wrapped.call(context, '1', '!')
  // @ts-expect-error The callback needs its receiver.
  wrapped.call({}, 1, '!')
  // @ts-expect-error A scheduled wrapper does not return the callback result.
  const value: string = wrapped.call(context, 1, '!')
}

tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowImportingTsExtensions": true,
    "lib": [
      "ES2022",
      "DOM"
    ],
    "types": []
  },
  "files": [
    "starter.mts",
    "types.mts"
  ]
}

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

Explore the full Higher-Order Functions material

Premium includes the complete lessons and implementation references. Free practice questions remain available without a subscription.

All course tracks & premium content
From basics to advanced masterclasses
Built for JS/TS developers like you
Real-world tips & common pitfalls
Upgrade to Premium