You already write small, focused functions — one trims a string, another parses it, a third validates the result. Each does a single job well, which is exactly what you want. The trouble begins when you need all three in a row. The honest way to combine them is to nest the calls: validate(parse(trim(input))). That works, but it reads inside-out and backwards, because the first function to run, trim, is the last name your eye lands on. Add a fourth stage and the line becomes a tower of parentheses that nobody enjoys debugging.
Composition is the name for that nesting, captured as a reusable tool. Instead of handwriting the pyramid every time, you describe the chain once and hand it a value. The chain becomes a thing in your program — something you can name, store in a variable, and reuse. This lesson shows the two shapes composition takes in JavaScript, the reading-order difference between them, and the one trap that catches almost everyone.
flowchart LR X["input x"] --> H["h(x) runs first"] H --> G["g(...) next"] G --> F["f(...) runs last"] F --> OUT["f(g(h(x)))"] style H fill:#1e7f3d,color:#fff style F fill:#3776ab,color:#fff style OUT fill:#1e293b,color:#fff
Composition runs right to left
A compose helper takes any number of functions and returns one new function that fires them in sequence. The rule that fixes the order is worth memorising: compose(f, g, h)(x) means f(g(h(x))). The rightmost function runs first, and the leftmost runs last to produce the final answer. That right-to-left order is not arbitrary — it matches how nested calls already work, so compose(f, g, h) is just f(g(h(...))) with the parentheses hidden.
Each function receives the output of the one to its right and nothing else: no shared variable, no accumulator you have to name. The value flows through the chain and emerges at the far end. Because every link depends only on its single input, you can test the whole pipeline one piece at a time, and you can swap any piece without disturbing the others.
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const shout = (s) => s.toUpperCase();
const exclaim = (s) => s + "!";
const trim = (s) => s.trim();
// RIGHT to LEFT: trim first, then exclaim, then shout
const loud = compose(shout, exclaim, trim);
console.log(loud(" hello ")); // HELLO!
console.log(compose(shout, exclaim, trim)(" hi ")); // HI!
Pipe reads left to right
Reading right to left feels natural to the machine and backwards to a human. When you describe a process out loud you almost always tell it in the order it happens: take the input, trim it, parse it, then validate it. The pipe helper honours that order. pipe(f, g, h)(x) means h(g(f(x))) — the leftmost function runs first, exactly the order in which you read the names.
Which one you reach for is a matter of taste and of what reads best at the call site. compose mirrors mathematical function notation, where the outer function is written first; pipe mirrors a recipe or a shell command, where the first step is written first. Internally they are the same idea, flipped end to end. The only thing that truly matters is consistency, because mixing the two in one file is a fast way to ship a bug.
flowchart LR X["input x"] --> F["f(x) runs first"] F --> G["g(...) next"] G --> H["h(...) runs last"] H --> OUT["h(g(f(x)))"] style F fill:#1e7f3d,color:#fff style H fill:#3776ab,color:#fff style OUT fill:#1e293b,color:#fff
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const trim = (s) => s.trim();
const splitWords = (s) => s.split(" ");
const count = (arr) => arr.length;
// LEFT to RIGHT: trim, then split, then count — reading order
const wordCount = pipe(trim, splitWords, count);
console.log(wordCount(" the quick brown fox ")); // 4
console.log(pipe(trim, splitWords, count)("hello world")); // 2
Building it yourself, and going point-free
There is no magic in compose or pipe — each is a reduce away. To pipe functions left to right, fold the list with a function that threads one result into the next: fns.reduce((acc, fn) => fn(acc), x). To compose, fold the same way but from the right, or simply reverse the list and pipe. Knowing the implementation matters, because it shows why an empty pipeline returns its input untouched and why the direction of the fold sets the order of execution.
A style that composition makes natural is point-free programming — describing a function by combining others, without ever naming the argument. Instead of const process = (x) => validate(parse(trim(x))), you write const process = pipe(trim, parse, validate). The data path is stated once, as a chain, and the argument stays implied. A reader sees the shape of the transformation at a glance.
flowchart LR S["a string"] --> A["stage returns<br/>an array"] A --> N["stage returns<br/>a number"] N --> OK["each output feeds<br/>the next input"] style S fill:#3776ab,color:#fff style A fill:#1e7f3d,color:#fff style N fill:#b45309,color:#fff
// pipe is just a reduce over the function list const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x); const double = (n) => n * 2; const addOne = (n) => n + 1; const halve = (n) => n / 2; // point-free: we name the data PATH, never the argument const mystery = pipe(double, addOne, halve); console.log(mystery(10)); // 20 -> 21 -> 10.5 console.log(pipe(double, addOne, halve)(4)); // 8 -> 9 -> 4.5
Write compose2(f, g) that returns a new function applying g first, then f — so compose2(f, g)(x) equals f(g(x)).
function compose2(f, g) {
// return a function that runs g, then f
}
g runs first, then f receives its result: return a function x => f(g(x)).
Double-check the reading order in the third test: which function sees 4?
function compose2(f, g) {
return (x) => f(g(x));
}
Write pipe(...fns) that takes any number of functions and returns a new function running them left to right — pipe(f, g, h)(x) equals h(g(f(x))). Use reduce so it works for any number of stages.
function pipe(...fns) {
// return a function that applies the fns left-to-right via reduce
}
reduce seeds with x: fns.reduce((acc, fn) => fn(acc), x).
Left to right means f sees x first, then g, then h.
function pipe(...fns) {
return (x) => fns.reduce((acc, fn) => fn(acc), x);
}
This pipeline should return the length of the longest word in an array, so longestLength(["hi", "hello"]) gives 5. But biggest receives an array while it is written as if it got a single number, so Math.max is handed the whole array and the answer is NaN. Fix biggest so the array from lengths is spread into Math.max.
function pipe(...fns) {
return (x) => fns.reduce((acc, fn) => fn(acc), x);
}
function longestLength(words) {
const lengths = (ws) => ws.map((w) => w.length); // [2, 5]
const biggest = (n) => Math.max(n); // BUG: n is an array
return pipe(lengths, biggest)(words);
}
lengths produces an array; biggest must spread it: Math.max(...arr).
Math.max([2,5]) is NaN because the array is coerced to a string; spread avoids that.
function pipe(...fns) {
return (x) => fns.reduce((acc, fn) => fn(acc), x);
}
function longestLength(words) {
const lengths = (ws) => ws.map((w) => w.length);
const biggest = (arr) => Math.max(...arr);
return pipe(lengths, biggest)(words);
}
Using a pipeline, write sumOfSquaresOfEvens(numbers) that returns the sum of the squares of the even numbers. For [1, 2, 3, 4] the evens are 2 and 4, their squares are 4 and 16, and the sum is 20. pipe is provided — compose three stages: keep the evens, square each, then sum. The data shape is array, array, array, then number.
function pipe(...fns) {
return (x) => fns.reduce((acc, fn) => fn(acc), x);
}
function sumOfSquaresOfEvens(numbers) {
// compose: evens -> squares -> total, using pipe(...)(numbers)
}
Three stages: filter the evens, map to squares, reduce to a sum.
The last stage must reduce the array to a single number with a 0 seed.
function pipe(...fns) {
return (x) => fns.reduce((acc, fn) => fn(acc), x);
}
function sumOfSquaresOfEvens(numbers) {
const evens = (ns) => ns.filter((n) => n % 2 === 0);
const squares = (ns) => ns.map((n) => n * n);
const total = (ns) => ns.reduce((a, n) => a + n, 0);
return pipe(evens, squares, total)(numbers);
}
Given pipe(f, g, h)(x), in what order do the functions run and what is the result?
pipe threads the value through the functions in the order they are written: f sees x first, its result feeds g, and g's result feeds h. compose is the mirror image that runs right to left.
Recap
- Composition chains functions so the output of one becomes the input of the next;
compose(f, g, h)(x)equalsf(g(h(x)))and runs right to left. - Pipe is the left-to-right twin:
pipe(f, g, h)(x)equalsh(g(f(x))), reading in the order the stages run. - Both are a
reduceover the list of functions, and an empty chain is the identity function. - Each stage's output type must match the next stage's input — a mismatch is the one trap, and JavaScript will not warn you.
- Point-free style states the data path once as a chain and lets the argument stay implied.
Next, design patterns collects the recurring shapes — factory, singleton, observer, strategy — that experienced JavaScript programmers reach for as a program grows.