Function Composition in JavaScript Explained

Function composition is a core functional programming pattern that enables developers to build complex logic by combining simpler, reusable functions. This article explains the fundamentals of function composition, demonstrates how to write it using standard JavaScript, and walks through creating custom compose and pipe helper utilities for cleaner, more readable code.

What is Function Composition?

Function composition is the process of passing the result of one function directly as the argument to another function. In mathematical terms, composing functions \(f\) and \(g\) results in \(f(g(x))\).

Instead of executing operations step-by-step and storing intermediate results in temporary variables, composition links pure functions together in a pipeline where data flows seamlessly from one stage to the next.

Basic Manual Composition

In its simplest form, you can compose functions in JavaScript by nesting them:

const double = (x) => x * 2;
const addTen = (x) => x + 10;

// Manual composition: addTen runs first, then double
const doubleAfterAddTen = (x) => double(addTen(x));

console.log(doubleAfterAddTen(5)); // Output: 30 ((5 + 10) * 2)

While manual nesting works for two functions, it becomes difficult to read and maintain when chaining many operations together (e.g., fn1(fn2(fn3(fn4(x))))).

Building a Generic compose Function

To compose any number of functions without messy nesting, you can create a reusable compose utility. Standard composition executes functions from right to left, matching standard mathematical notation.

Using JavaScript’s Array.prototype.reduceRight():

const compose = (...functions) => (initialValue) =>
  functions.reduceRight((accumulator, currentFn) => currentFn(accumulator), initialValue);

Example Usage:

const trim = (str) => str.trim();
const toLowerCase = (str) => str.toLowerCase();
const wrapInDiv = (str) => `<div>${str}</div>`;

const formatText = compose(wrapInDiv, toLowerCase, trim);

console.log(formatText("   HELLO WORLD   ")); 
// Output: "<div>hello world</div>"

In this example, execution flows from right to left: trim runs first, followed by toLowerCase, and finally wrapInDiv.

The pipe Alternative (Left to Right)

Many developers find left-to-right execution more intuitive because it reflects the standard reading order. In functional programming, this is known as piping.

Using JavaScript’s Array.prototype.reduce():

const pipe = (...functions) => (initialValue) =>
  functions.reduce((accumulator, currentFn) => currentFn(accumulator), initialValue);

Example Usage:

const normalizeUsername = pipe(
  (str) => str.trim(),
  (str) => str.toLowerCase(),
  (str) => str.replace(/\s+/g, "_")
);

console.log(normalizeUsername("  John Doe  ")); 
// Output: "john_doe"

Key Rules for Effective Function Composition

  1. Unary Functions: Composed functions should ideally be unary (accepting a single argument). For functions requiring multiple parameters, use currying to convert them into unary functions before composition.
  2. Pure Functions: Each function in the pipeline should be pure—free of side effects and deterministic—to ensure reliable and predictable behavior.
  3. Type Consistency: The output type of one function must match the expected input type of the subsequent function in the chain.