Higher-Order Functions Interview Questions & Answers
31 questions Updated 2026-06-18
JavaScript higher-order function interview questions — callbacks, map/filter/ reduce, composition, currying, partial application, memoization, debounce and throttle, plus classic callback pitfalls.
Read the in-depth guideJavaScript Higher-Order Functions — Currying, Composition, Memoization and MoreA higher-order function (HOF) is a function that does at least one of two things: takes one or more functions as arguments, or returns a function. Any function that only deals with plain values is first-order.
// takes a function (callback)
[1, 2, 3].map(n => n * 2) // map is a HOF
// returns a function
const multiplier = factor => n => n * factor // HOF returning a HOF
const double = multiplier(2)
double(5) // 10
HOFs are possible because functions in JavaScript are first-class values — they can be stored, passed, and returned like any other value. They are the backbone of functional-style JS and enable reuse without copy-pasting logic.
A callback is a function passed into another function so it can be called back later — either synchronously (now) or asynchronously (later).
// synchronous callback
[1, 2, 3].forEach(n => console.log(n)) // called once per element, now
// asynchronous callback
setTimeout(() => console.log('later'), 0) // called after the timer fires
The function receiving the callback decides when and with what arguments to invoke it. A common gotcha: a synchronous-looking API may call the callback asynchronously, so don't assume code after the call has the callback's results yet.
map is a HOF because it accepts a function and applies it to every
element, returning a new array of the results. It abstracts the loop.
const nums = [1, 2, 3]
const squared = nums.map(n => n * n) // [1, 4, 9]
// nums is unchanged — map does not mutate
The callback receives (element, index, array). Pitfall: map is for
transforming into a new array — if you only need side effects (logging,
pushing to the DOM) use forEach instead, otherwise you allocate an array of
undefined you never use.
filter is a HOF that takes a predicate (a function returning a boolean)
and returns a new array containing only the elements for which the
predicate is truthy.
const nums = [1, 2, 3, 4]
const evens = nums.filter(n => n % 2 === 0) // [2, 4]
The result's truthiness is what matters, not strict true. Pitfall:
returning a non-boolean accidentally — e.g. filter(n => n.id) keeps elements
with truthy id, which silently drops items whose id is 0 or ''.
reduce is the most general array HOF: it takes a reducer function
(accumulator, element) => newAccumulator plus an initial value, and folds
the whole array down to a single value (which can itself be an array or
object).
const sum = [1, 2, 3, 4].reduce((acc, n) => acc + n, 0) // 10
// map and filter can both be expressed via reduce
const doubled = [1, 2, 3].reduce((acc, n) => [...acc, n * 2], []) // [2,4,6]
Pitfall: omitting the initial value. Without it, the first element becomes
the accumulator and iteration starts at index 1 — and reduce on an empty
array with no initial value throws a TypeError.
They separate the iteration mechanism from the per-element logic. The HOF owns the loop, bounds checking, and array allocation; you supply only the small piece that varies. This is the template method idea expressed with functions.
// imperative: loop boilerplate repeated everywhere
const out = []
for (let i = 0; i < users.length; i++) out.push(users[i].name)
// declarative: intent is obvious, no boilerplate
const names = users.map(u => u.name) //
Because the callback is just a value, you can name it, test it in isolation, and reuse it across many call sites — reducing duplication and bugs.
Composition combines small functions into a bigger one, where the output
of each becomes the input of the next — compose(f, g)(x) === f(g(x)).
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x)
const clean = pipe(s => s.trim(), s => s.toLowerCase())
clean(' HELLO ') // 'hello' reads left-to-right
compose runs right-to-left (math convention); pipe runs
left-to-right (reads like a pipeline). Pitfall: each function should
take and return a single value — composition breaks down with multi-arg
functions unless you curry them first.
Currying transforms a function of N arguments into N nested functions that each take one argument and return the next function until all args are collected.
const add = a => b => c => a + b + c // curried
add(1)(2)(3) // 6
// vs the normal form
const addN = (a, b, c) => a + b + c
addN(1, 2, 3) // 6
Currying enables partial application and clean composition, because each stage returns a specialized function. Pitfall: deeply curried APIs hurt readability and are harder to debug — reserve currying for cases where the partial-application benefit is real.
Partial application fixes some of a function's arguments now, producing a new function that takes the rest later. Currying is stricter: it always breaks a function into a chain of one-argument functions.
const greet = (greeting, name) => `${greeting}, ${name}!`
const hi = greet.bind(null, 'Hi') // partial application via bind
hi('Sam') // 'Hi, Sam!' greeting already fixed
So every curried call is a partial application, but partial application can
fix several arguments at once and isn't restricted to one-at-a-time. bind is
the built-in tool for it (ignoring the this argument here with null).
Point-free style defines functions without naming their arguments, building them by composing other functions instead. The "point" is the data argument you don't mention.
// pointed
const isOdd = n => n % 2 === 1
const countOdds = arr => arr.filter(isOdd).length
// point-free helper composed from smaller pieces
const prop = key => obj => obj[key]
const names = users.map(prop('name')) // no obj parameter named
It can read very cleanly, but pitfall: taken too far it becomes cryptic and stack traces lose meaningful names. Use it where it genuinely clarifies.
Memoization caches a pure function's results keyed by its arguments, so repeated calls with the same input return instantly instead of recomputing. A HOF wraps the original function and keeps the cache in a closure.
function memoize(fn) {
const cache = new Map()
return function (arg) {
if (cache.has(arg)) return cache.get(arg) // cache hit
const result = fn.call(this, arg)
cache.set(arg, result)
return result
}
}
Pitfall: it only works for pure functions (same input -> same output) and the naive version keys on a single primitive arg — multi-arg or object keys need a serialization strategy and risk unbounded memory growth.
Debounce returns a wrapped function that delays calling the original until activity has stopped for a quiet period; each new call resets the timer. It's a HOF: it takes a function and returns a closure holding the timer.
function debounce(fn, wait) {
let timer
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), wait) // resets
}
}
const onSearch = debounce(query => fetchResults(query), 300)
Great for search-as-you-type or resize handlers. Pitfall: the wrapper must
forward this and args with apply, or the debounced handler loses its
context and the event arguments.
Throttle guarantees the function runs at most once per interval, no matter how often it's called — useful for scroll or mousemove. Debounce instead waits until calls stop. Both are HOFs returning a stateful closure.
function throttle(fn, interval) {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last >= interval) { // fire on leading edge
last = now
fn.apply(this, args)
}
}
}
Rule of thumb: throttle for steady sampling during continuous events; debounce for "do it once they're done" events.
once returns a wrapper that lets the underlying function run only the
first time; later calls return the cached first result and skip execution.
The "have I run yet" flag and result live in a closure.
function once(fn) {
let called = false, result
return function (...args) {
if (!called) {
called = true
result = fn.apply(this, args) // runs exactly once
}
return result
}
}
const init = once(() => console.log('setup'))
init(); init() // logs 'setup' only once
Handy for idempotent initialization. Pitfall: the result is cached forever
— if the first call threw, decide whether called should stay false to allow
a retry.
Returning a function lets you pre-configure behavior now and defer execution to later, capturing the configuration in a closure. This is the mechanism behind factories, currying, and middleware.
function makeTagger(tag) {
return content => `<${tag}>${content}</${tag}>` // remembers `tag`
}
const h1 = makeTagger('h1')
h1('Hello') // '<h1>Hello</h1>' tag captured
The returned function is specialized without you rewriting it. The key enabler is the closure: each returned function keeps its own copy of the captured configuration.
A synchronous callback runs to completion before the HOF returns (like
map's callback). An asynchronous callback is scheduled and runs later,
after the current call stack clears (like setTimeout or a fetch handler).
console.log('A')
[1].forEach(() => console.log('B')) // sync -> B before C
setTimeout(() => console.log('D'), 0) // async -> D last
console.log('C')
// order: A, B, C, D
Pitfall: mixing them in one API ("sometimes sync, sometimes async") causes subtle bugs — choose one contract. This is also why callback-based async code led to "callback hell" and motivated Promises.
map calls the callback with three arguments — (value, index, array) —
but parseInt(string, radix) reads the second as a radix. So you actually
call parseInt('1', 0), parseInt('2', 1), parseInt('3', 2).
['1', '2', '3'].map(parseInt)
// parseInt('1', 0) -> 1 (radix 0 ignored -> base 10)
// parseInt('2', 1) -> NaN (radix 1 invalid)
// parseInt('3', 2) -> NaN ('3' not a binary digit)
// result: [1, NaN, NaN]
Fix: wrap it so only the value is passed:
['1','2','3'].map(s => parseInt(s, 10)) -> [1, 2, 3]. The lesson: be
careful passing a multi-arity built-in directly as a callback.
Passing obj.method extracts the function value alone — the obj
receiver is not attached. When the HOF later calls it as a plain function,
this is undefined (strict) or the global object.
const counter = {
count: 0,
inc() { this.count++ }
}
[1, 2].forEach(counter.inc) // `this` is not `counter`
Fixes: bind it — forEach(counter.inc.bind(counter)) — or wrap in an
arrow that preserves the receiver — forEach(() => counter.inc()). The
root cause is that this is determined by how a function is called, not
where it was defined.
Method chaining (arr.filter(...).map(...).reduce(...)) is composition
where each step is a method returning a chainable value. Standalone
composition (pipe(f, g, h)) works on free functions and any data type,
not just arrays.
// chaining
const total = orders.filter(o => o.paid).map(o => o.amount)
.reduce((a, b) => a + b, 0) // reads top-to-bottom
// composition with free functions
const total2 = pipe(
os => os.filter(o => o.paid),
os => os.map(o => o.amount),
os => os.reduce((a, b) => a + b, 0)
)(orders)
Chaining is ergonomic when the methods exist; composition is more general and decouples logic from the data's prototype. Pitfall: long chains over large arrays create many intermediate arrays.
flatMap is a HOF that maps then flattens one level in a single pass. It's
equivalent to arr.map(fn).flat() but more efficient and expressive.
const sentences = ['hello world', 'foo bar']
const words = sentences.flatMap(s => s.split(' '))
// ['hello', 'world', 'foo', 'bar'] flattened
// also handy to map-and-filter: return [] to drop
[1, 2, 3].flatMap(n => n % 2 ? [n] : []) // [1, 3]
Pitfall: it only flattens one level — deeply nested results need
flat(Infinity) afterwards.
Both are HOFs taking a predicate. every returns true only if the
predicate passes for all elements; some returns true if at least
one passes. Both short-circuit.
[2, 4, 6].every(n => n % 2 === 0) // true
[1, 2, 3].some(n => n > 2) // true (stops at 3)
every stops at the first falsy result; some stops at the first truthy
result. Edge case: [].every(...) is true (vacuous truth) and
[].some(...) is false.
Both fix the receiver. bind creates a new bound function once, ideal when
you need a stable reference (e.g. to later removeEventListener). An arrow
wrapper is created fresh each render/call and is convenient for forwarding
changing arguments.
// stable, removable
const handler = this.onClick.bind(this)
el.addEventListener('click', handler)
el.removeEventListener('click', handler) // same reference
// arrow re-created each time -> cannot remove
el.addEventListener('click', () => this.onClick()) // no handle to remove
Pitfall: binding inside JSX/render or in a loop creates a new function every time, which can defeat memoization and break listener removal.
identity = x => x returns its argument unchanged; constant = x => () => x
returns a function that always yields x. They are tiny building blocks
that make HOF pipelines uniform.
const identity = x => x
// filter out falsy values with no custom predicate
['a', '', 'b', null].filter(identity) // ['a', 'b']
const always5 = (() => () => 5)()
[1, 2, 3].map(always5) // [5, 5, 5]
They shine as default callbacks (e.g. a sort key defaulting to identity) and in composition where you need a no-op transform.
Data-last means the data parameter comes last, so the earlier configuration args can be partially applied to build reusable, composable functions. The data flows in only at the end of a pipeline.
const map = fn => arr => arr.map(fn) // data (arr) last
const filter = pred => arr => arr.filter(pred)
const process = pipe(filter(x => x > 0), map(x => x * 2))
process([-1, 2, 3]) // [4, 6] data supplied last
Native array methods are data-first (arr.map(fn)), which is why you
often wrap them. The trade-off: data-last composes beautifully but reads
backwards from the method style most JS developers know.
The Node-style error-first callback convention passes the error as the
first argument and the result(s) afterward: callback(err, data). If
err is truthy, something failed.
fs.readFile('x.txt', (err, data) => {
if (err) return handle(err) // check error first
use(data)
})
Pitfall: forgetting to return after handling the error lets the success
path run with undefined data. This convention predates Promises, which
largely replaced it with .then/.catch and async/await.
Many array HOFs accept an optional thisArg as their last parameter. When
provided (and the callback is a regular function, not an arrow), this inside
the callback is set to it.
const ctx = { factor: 3 }
[1, 2, 3].map(function (n) { return n * this.factor }, ctx) // [3, 6, 9]
Pitfall: thisArg is ignored by arrow callbacks, because arrows take
this lexically. Most modern code just uses an arrow that closes over the
value instead of relying on thisArg.
Plain compose/pipe assume synchronous functions. For async, each step
returns a Promise, so you await between stages — often by reducing over an
initial resolved Promise.
const pipeAsync = (...fns) => x =>
fns.reduce((acc, fn) => acc.then(fn), Promise.resolve(x))
const run = pipeAsync(fetchUser, u => fetchPosts(u.id), posts => posts.length)
run(42).then(console.log) // each step awaits the previous
Pitfall: mixing sync and async functions in one pipe is fine here because
then auto-wraps non-Promise returns — but an unhandled rejection in any
stage rejects the whole chain, so attach a .catch.
A generic curry collects arguments until it has received as many as the
function's declared arity (fn.length), then invokes it. Until then it
returns a function that accumulates more args.
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn.apply(this, args)
return (...next) => curried.apply(this, [...args, ...next]) //
}
}
const add = curry((a, b, c) => a + b + c)
add(1)(2)(3) // 6
add(1, 2)(3) // 6 — flexible grouping
Pitfall: it relies on fn.length, which excludes rest params and
parameters with defaults — so currying variadic functions doesn't work without
passing an explicit arity.
Instead of subclassing to share behavior, you can wrap functions with HOFs (decorators) to add cross-cutting concerns like logging, caching, or retries — composition over inheritance.
const withLogging = fn => (...args) => {
console.log('call', fn.name, args)
return fn(...args)
}
const withRetry = fn => async (...args) => {
try { return await fn(...args) } catch { return fn(...args) }
}
const robustFetch = withLogging(withRetry(fetchData)) // stacked
Each concern is an independent, testable HOF you can stack in any order. This avoids deep inheritance hierarchies and the fragile base-class problem.
tap runs a side effect (like logging) on a value and then returns the
value unchanged, so you can drop it into a composition without altering the
data flow.
const tap = fn => x => { fn(x); return x }
const result = pipe(
x => x + 1,
tap(x => console.log('after +1:', x)), // peeks, passes through
x => x * 2
)(5) // logs 6, result 12
It's the functional equivalent of a breakpoint. Pitfall: because it's for
side effects, don't accidentally return fn(x) — that would replace the value
and corrupt the pipeline.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.