Functional Programming in JS
Functional programming (FP) is a style of writing code as the evaluation of functions rather than a sequence of statements that mutate shared state. JavaScript is not a “pure” functional language, but it has everything FP needs: functions are values, closures capture context, and array methods encourage transformation over iteration. Embracing even a few FP ideas — pure functions, immutability, and composition — tends to make code more testable, predictable, and reusable. This page sets up the section by framing those ideas; the linked pages dig into each one.
Functions are first-class values
The foundation of FP in JavaScript is that functions are first-class citizens: you can store them in variables, pass them as arguments, return them from other functions, and put them in arrays or objects — exactly like any number or string.
const double = (n) => n * 2;
// Stored in a variable, passed as an argument, returned from a function.
const apply = (fn, value) => fn(value);
const adder = (step) => (n) => n + step;
console.log(apply(double, 21)); // function passed in
console.log(adder(5)(10)); // function returned, then called
Output:
42
15
A function that takes or returns another function is a higher-order function. They are the building blocks of everything else here: map, filter, reduce, currying, and composition are all just higher-order functions.
Pure functions
A function is pure when its output depends only on its inputs and it produces no side effects — no mutating external variables, no writing to the DOM, no network calls, no logging. Given the same arguments, a pure function always returns the same result.
// Pure: depends only on its arguments, changes nothing outside.
const add = (a, b) => a + b;
// Impure: reads and mutates external state.
let total = 0;
const addToTotal = (n) => {
total += n; // side effect
return total;
};
Purity is what makes code easy to reason about. add(2, 3) can be replaced by 5 anywhere without changing behaviour — a property called referential transparency. It also makes testing trivial: no mocks, no setup, just inputs and an expected output.
Tip: You can’t (and shouldn’t) make everything pure — programs need effects to be useful. The goal is to push side effects to the edges of your program and keep the core logic pure.
Immutability
Immutability means you don’t change existing data; you create new data instead. Mutation is the most common source of “spooky action at a distance” bugs, where one part of the code changes an object another part still relies on.
const user = { name: "Ada", roles: ["admin"] };
// Mutating (avoid): everyone sharing `user` is affected.
// user.name = "Grace";
// Immutable update: produce a new object, leave the original intact.
const renamed = { ...user, name: "Grace" };
const promoted = { ...user, roles: [...user.roles, "owner"] };
console.log(user.name, renamed.name);
console.log(promoted.roles);
Output:
Ada Grace
[ 'admin', 'owner' ]
Spread syntax, Array.prototype.map/filter/concat, and Object.freeze all support working without mutation. Treating data as immutable pairs naturally with pure functions, since a pure function must not mutate its arguments.
Declarative over imperative
Imperative code describes how to do something step by step; declarative code describes what you want and lets the building blocks handle the how. FP pushes you toward the declarative end.
const numbers = [1, 2, 3, 4, 5, 6];
// Imperative: manual loop, mutable accumulator.
const evensImperative = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) evensImperative.push(numbers[i] * 10);
}
// Declarative: express intent as a pipeline.
const evensDeclarative = numbers
.filter((n) => n % 2 === 0)
.map((n) => n * 10);
console.log(evensDeclarative);
Output:
[ 20, 40, 60 ]
The declarative version reads as a sentence — “keep the evens, then multiply each by ten” — and has no loop index or mutable array to get wrong.
Composition
Composition is combining small, single-purpose functions into larger behaviour. Instead of one big function, you write tiny pure functions and wire them together so the output of one feeds the next.
const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const slugify = (s) => s.replace(/\s+/g, "-");
// pipe: left-to-right composition.
const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input);
const toSlug = pipe(trim, lower, slugify);
console.log(toSlug(" Hello World "));
Output:
hello-world
Each step is independently testable and reusable, and the pipeline makes the data flow explicit.
How the ideas fit together
| Concept | What it gives you | Enables |
|---|---|---|
| First-class functions | Functions as data | Higher-order functions, composition |
| Pure functions | Same input - same output | Easy testing, caching, parallelism |
| Immutability | Data never changes underneath you | Safe sharing, predictable state |
| Declarative style | Intent over mechanics | Readable, less error-prone code |
| Composition | Small parts - big behaviour | Reuse, single responsibility |
Gotcha: FP is a spectrum, not an all-or-nothing rule. Adopting pure functions and immutability for your core logic delivers most of the benefit without rewriting your whole codebase in a point-free style.
Best Practices
- Prefer pure functions for business logic; isolate side effects (I/O, DOM, network) at the boundaries.
- Default to immutable updates with spread syntax and non-mutating array methods over in-place mutation.
- Reach for
map,filter, andreduceinstead of manualforloops when transforming collections. - Build behaviour from small, named, single-purpose functions and compose them rather than writing monoliths.
- Avoid relying on shared mutable state; pass data in and return new data out.
- Keep functions free of hidden inputs — depend on arguments, not on closures over changing variables.
- Lean on referential transparency: if a pure call is expensive, you can safely memoize it.