Module 8 · Functional & Advanced Patterns ⏱ 18 min

Composition & Pipe

By the end of this lesson you will be able to:
  • Combine small functions into a pipeline so the output of one feeds the next
  • Distinguish compose, which runs right to left, from pipe, which runs left to right
  • Diagnose the type-mismatch trap where a stage receives a value of the wrong shape

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
compose runs right to left: the rightmost function sees the input first, the leftmost produces the final answer.

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.

compose in action. Press Run — note the order the functions fire.
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
pipe is the readable twin: functions fire in the order you read them, left to right.
pipe in action. The value flows through the stages in reading order.
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
The shape of the data transforms as it flows: each stage must accept the type the previous stage produced.
pipe is a reduce; point-free style names the path, not the argument.
// 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
Exercise

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
}
Exercise

Write pipe(...fns) that takes any number of functions and returns a new function running them left to rightpipe(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
}
Exercise

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);
}
Exercise

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)
}
Exercise

Given pipe(f, g, h)(x), in what order do the functions run and what is the result?

Recap

  • Composition chains functions so the output of one becomes the input of the next; compose(f, g, h)(x) equals f(g(h(x))) and runs right to left.
  • Pipe is the left-to-right twin: pipe(f, g, h)(x) equals h(g(f(x))), reading in the order the stages run.
  • Both are a reduce over 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.

Checkpoint quiz

What does pipe(f, g, h)(x) evaluate to?

A pipeline produces NaN where you expected a number. What is the most likely cause?

Go deeper — technical resources