Functional Programming in JavaScript
May 12, 2026 · 2 min read
Functional programming in JavaScript gets a reputation for being academic, but the parts that actually pay off day to day are small and concrete: keep functions pure, treat data as immutable, and build behaviour by composing little pieces. You don't need a category-theory glossary to get most of the value.
Pure functions
A pure function depends only on its arguments and produces only its return value — no reading from the outside, no writing to it. Same input, same output, every time.
// impure: reaches outside, mutates the clock
const addTimestamp = (event) => {
event.at = Date.now()
return event
}
// pure: everything it touches comes in and goes out
const withTimestamp = (event, now) => ({ ...event, at: now })The payoff is testing. A pure function needs no mocks, no setup, no teardown — you pass values and assert on values. That's the whole test.
Immutability
Mutation is the quiet source of "it worked a second ago" bugs: something holds a reference, something else changes it, and the two halves of your program disagree about reality. Returning new data instead of editing the old keeps that from happening.
// don't mutate the input
const activate = (users, id) =>
users.map((u) => (u.id === id ? { ...u, active: true } : u))The array methods that return — map, filter, reduce, flatMap — are
your default toolkit. The ones that mutate in place — push, splice, sort,
reverse — are the ones to use deliberately, on data you own.
Composition over pipelines of statements
Once functions are small and pure, you assemble them. Composition is just "feed the output of one into the next," and it reads top-to-bottom instead of inside-out.
const pipe =
(...fns) =>
(x) =>
fns.reduce((acc, fn) => fn(acc), x)
const slugify = pipe(
(s) => s.trim().toLowerCase(),
(s) => s.replace(/\s+/g, "-"),
(s) => s.replace(/[^a-z0-9-]/g, ""),
)
slugify(" Hello, World! ") // "hello-world"Each step is independently testable, independently nameable, and trivially reorderable.
Higher-order functions
Functions that take or return functions let you factor out the pattern and
keep the specifics at the call site. It's the same idea behind map and
filter, applied to your own code.
const withRetry =
(fn, times = 3) =>
async (...args) => {
let lastError
for (let i = 0; i < times; i++) {
try {
return await fn(...args)
} catch (err) {
lastError = err
}
}
throw lastError
}
const fetchUser = withRetry((id) => fetch(`/api/users/${id}`))Knowing where to stop
JavaScript is multi-paradigm, and that's a feature. A for loop is sometimes
clearer than a clever reduce; a small, local mutation inside an otherwise-pure
function can be perfectly fine. The goal isn't purity for its own sake — it's
code you can reason about without holding the whole program in your head.
Push the side effects to the edges and keep the middle pure. The center of
your app becomes a set of plain data transformations, and the messy parts —
I/O, the DOM, the network — stay at the boundary where you can see them.