Function Composition
Function composition is the simple idea of taking the output of one function and feeding it into another. f(g(x)) is composition by hand. The reason it matters: once you have small, focused functions, composition is how you build big behaviour without writing big functions. JavaScript has no built-in operator for it — but two tiny helpers cover almost every case.
compose and pipe from scratch
Both take a list of functions and return a new function that runs them in order. compose runs them right-to-left (matching the math notation f ∘ g); pipe runs them left-to-right (easier to read).
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const trim = s => s.trim();
const lower = s => s.toLowerCase();
const dashes = s => s.replace(/\s+/g, "-");
const slug = pipe(trim, lower, dashes);
slug(" Hello World "); // "hello-world"
const slug2 = compose(dashes, lower, trim);
slug2(" Hello World "); // "hello-world"Both produce the same result. Prefer pipe for readability — your eye follows the data through the steps in the order it actually flows.
Why composing tiny functions wins
Reusability —
trimworks on any string. Snap it into any pipeline.Testability — each step is a pure function with no surrounding context to mock.
Readability — the pipeline reads as a list of named transformations.
Changeability — swap one step out, reorder them, drop one — the others do not care.
A wider example
const trim = s => s.trim();
const noTags = s => s.replace(/<[^>]*>/g, "");
const noExtra = s => s.replace(/\s+/g, " ");
const clip = n => s => s.length > n ? s.slice(0, n) + "…" : s;
const summarise = pipe(trim, noTags, noExtra, clip(80));
summarise(" <b>Hello</b> world from space ");
// "Hello world from space"Each step is one regex or one slice. Nothing is hidden, every step is reusable elsewhere, and you can read the pipeline like an English sentence.
Composing async functions
If steps return promises, the helpers need to await. A small pipeAsync does the job.
const pipeAsync = (...fns) => x =>
fns.reduce(async (acc, fn) => fn(await acc), x);
const loadUser = id => fetch("/users/" + id).then(r => r.json());
const getPosts = user => fetch("/users/" + user.id + "/posts").then(r => r.json());
const titles = posts => posts.map(p => p.title);
const userTitles = pipeAsync(loadUser, getPosts, titles);
userTitles(42).then(console.log);The synchronous functions still slot into the pipeline because await on a non-promise simply returns it.
Composition vs method chaining
Arrays already let you chain with dots: .filter(...).map(...).reduce(...). That works as long as every step is a method on the value. Composition is the more general version — the functions can come from anywhere, and the value can be any shape.
// Method chain — only works because each step returns an array. [1, 2, 3, 4].filter(n => n > 1).map(n => n * 2).reduce((a, b) => a + b, 0); // Pipe — works on any value, with any helpers. const sumOfBigDoubles = pipe( arr => arr.filter(n => n > 1), arr => arr.map(n => n * 2), arr => arr.reduce((a, b) => a + b, 0), ); sumOfBigDoubles([1, 2, 3, 4]); // 18
Composition with multiple arguments
A composed pipeline naturally takes one argument and returns one. For multi-argument steps, curry them first — that is one reason currying and composition often appear together.
const replaceC = (pattern, replacement) => s => s.replace(pattern, replacement);
const sanitise = pipe(
replaceC(/<[^>]*>/g, ""), // strip HTML
replaceC(/\s+/g, " "), // collapse spaces
s => s.trim(),
);
sanitise(" <b>hi</b> world "); // "hi world"When not to compose
Steps that branch on intermediate values are awkward to express as a flat pipe.
Composing functions that already exist as methods (
.filter,.map) is rarely shorter than chaining.If you need to thread several values through, you are using the wrong tool — pass an object instead.