JavaScript Currying and Composition: Functions That Chain Cleanly

Currying and composition help you build small, reusable JavaScript functions that are easier to test, combine, and reason about. Together, they are a core part of functional programming style and show up in real code any time you want to transform data in steps.

Quick answer: Currying turns a function that takes many arguments into a chain of functions that each take one argument. Composition combines functions so the output of one becomes the input of the next.

Difficulty: Intermediate

You'll understand this better if you know: how JavaScript functions return values, how arrow functions work, and the difference between arguments and parameters.

1. What Is Currying and Composition?

Currying and composition are two related but different ways to structure function logic in JavaScript.

For example, a curried function can create a specialized function like “format money in USD,” and composition can build a transformation pipeline like “trim, lowercase, then validate.”

2. Why Currying and Composition Matter

These patterns matter because they reduce repetition and make logic easier to reuse.

Instead of writing many slightly different functions, you can build one flexible function and specialize it with currying. Instead of writing a long block of step-by-step transformation code, you can compose smaller functions into a readable flow.

They are especially useful when you:

They are less useful when the logic is highly interactive, heavily stateful, or clearer as a simple straight-line function.

3. Basic Syntax or Core Idea

Currying a function

A normal function may take multiple parameters at once. A curried version returns another function after each argument until the final result is produced.

const add = a => b => a + b;

const result = add(2)(3); // 5

Here, add does not take two arguments at once. It takes one, returns a function, then takes the next one.

Composing functions

A simple composition helper passes a value through functions from right to left.

const compose = (...fns) => value => fns.reduceRight((acc, fn) => fn(acc), value);

With composition, the output of one function becomes the input of the next function in the chain.

4. Step-by-Step Examples

Example 1: Building a specialized formatter

Currying is useful when the same operation needs a fixed setting, such as a currency code, locale, or rounding rule.

const formatCurrency = locale => currency => amount =>
  new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(amount);

const formatUSD = formatCurrency("en-US")("USD");

console.log(formatUSD(19.99));

This creates a reusable formatter configured for one locale and currency, so later calls only need the amount.

Example 2: Composing text cleanup steps

Composition is a good fit for formatting strings in a predictable order.

const trim = text => text.trim();
const toLowerCase = text => text.toLowerCase();
const collapseSpaces = text => text.replace(/\s+/g, " ");

const pipe = (...fns) => value =>
  fns.reduce((acc, fn) => fn(acc), value);

const normalize = pipe(trim, toLowerCase, collapseSpaces);

console.log(normalize("  Hello   WORLD  ")); // "hello world"

This is easier to read than repeating each transformation inline every time you need it.

Example 3: Combining validation rules

You can use composition to keep validation steps separate and reusable.

const isNotEmpty = text => text.trim() !== "";
const hasAtSymbol = text => text.includes("@");
const hasDotAfterAt = text => /@.+\./.test(text);

const and = (...checks) => value => checks.every((check) => check(value));

const isEmailLike = and(isNotEmpty, hasAtSymbol, hasDotAfterAt);

console.log(isEmailLike("[email protected]")); // true

The separate checks are easier to test and replace than a single large validation expression.

Example 4: Reusing a curried predicate in array methods

Currying can create reusable functions for array filtering and searching.

const greaterThan = limit => value => value > limit;

const numbers = [3, 8, 12, 5];
const greaterThanFive = greaterThan(5);
const filtered = numbers.filter(greaterThanFive);

console.log(filtered); // [8, 12]

This works well because filter expects a function that receives one value at a time.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Returning a value too early in a curried function

Beginners sometimes write a function that looks curried but still tries to use all inputs at once. That breaks the whole purpose of currying.

Problem: This function returns a number immediately, so calling it one argument at a time does not work.

const add = (a, b) => a + b;

const sum = add(2)(3);

Fix: Return a function for each missing argument.

const add = a => b => a + b;

const sum = add(2)(3);

The corrected version works because each call receives exactly one argument and returns the next function.

Mistake 2: Using composition with non-function values

Composition only works when every item in the chain is a function. Passing a plain value breaks the pipeline.

Problem: This code puts a number inside the composed chain, so the pipeline cannot call it like a function.

const compose = (...fns) => value =>
  fns.reduceRight((acc, fn) => fn(acc), value);

const double = n => n * 2;
const broken = compose(double, 3);

Fix: Keep every step as a function and pass the initial value separately.

const compose = (...fns) => value =>
  fns.reduceRight((acc, fn) => fn(acc), value);

const double = n => n * 2;
const triple = n => n * 3;
const doubleThenTriple = compose(triple, double);

console.log(doubleThenTriple(4)); // 24

The fixed version works because composition receives only functions and one starting value.

Mistake 3: Confusing function order in composition

Composition order matters. If you place the functions in the wrong direction, you can get the wrong result even when the code still runs.

Problem: This example trims after lowercasing, which is fine, but the logic can become wrong when order affects the output shape.

const compose = (...fns) => value =>
  fns.reduceRight((acc, fn) => fn(acc), value);

const trim = text => text.trim();
const addPrefix = text => "ID: " + text;

const wrong = compose(trim, addPrefix);
console.log(wrong(" 42 "));

Fix: Put the transformation steps in the order you want the data to flow, or use a left-to-right helper if that is easier to read.

const pipe = (...fns) => value =>
  fns.reduce((acc, fn) => fn(acc), value);

const pipeResult = pipe(trim, addPrefix)(" 42 ");
console.log(pipeResult); // "ID: 42"

The corrected version is easier to reason about because the data flow matches the order of the function list.

7. Best Practices

Practice 1: Keep curried functions small and purpose-driven

Currying is most useful when each layer represents one meaningful choice, such as locale, prefix, or threshold.

const createLogger = level => message =>
  `[${level}] ${message}`;

const warn = createLogger("WARN");

Small layers make the function easier to reuse and test.

Practice 2: Prefer one-input functions for composition

Composition is simplest when every step takes a single value and returns a single value.

const sanitize = text => text.trim();
const escapeHtml = text => text.replace(/&/g, "&");

Single-input functions are easier to compose and less likely to create argument-order bugs.

Practice 3: Name intermediate functions clearly

Even though currying can reduce repetition, unreadable one-line chains can hurt maintainability. Good names make the pipeline obvious.

const isAdult = minAge => age => age >= minAge;
const isAllowedToVote = isAdult(18);

Clear names help other developers understand the intent without tracing every function call.

8. Limitations and Edge Cases

Note: In JavaScript, composition and currying are conventions, not special language features. You build them yourself with ordinary functions.

9. Practical Mini Project

Let’s build a small text-processing pipeline that trims input, normalizes spacing, and adds a label. This shows how currying and composition work together in a realistic utility.

const pipe = (...fns) => value =>
  fns.reduce((acc, fn) => fn(acc), value);

const trim = text => text.trim();
const collapseSpaces = text => text.replace(/\s+/g, " ");
const toTitleCase = text =>
  text.split(" ").map((word) =>
    word.charAt(0).toUpperCase() + word.slice(1)
  ).join(" ");

const prefixWith = prefix => text => `${prefix}: ${text}`;

const formatHeadline = pipe(trim, collapseSpaces, toTitleCase, prefixWith("Headline"));

console.log(formatHeadline("   currying    and   composition   "));

This pipeline is fully reusable: the base operations stay separate, prefixWith is curried, and pipe composes the steps into one function.

10. Key Points

11. Practice Exercise

Create a curried function and a composed pipeline for a simple profile formatter.

Expected output: User: Jane Doe

Hint: Start by trimming the input, convert it to title case, and then apply the prefix at the end.

Solution:

const pipe = (...fns) => value =>
  fns.reduce((acc, fn) => fn(acc), value);

const trim = text => text.trim();
const toTitleCase = text =>
  text.split(" ").map((word) =>
    word.charAt(0).toUpperCase() + word.slice(1)
  ).join(" ");

const addPrefix = prefix => text => `${prefix}: ${text}`;

const formatUserName = pipe(
  trim,
  toTitleCase,
  addPrefix("User")
);

console.log(formatUserName("  jane doe  ")); // "User: Jane Doe"

12. Final Summary

Currying and composition are foundational JavaScript techniques for building small, reusable functions. Currying helps you specialize a function by supplying arguments one at a time, while composition lets you connect simple functions into a clear data flow.

Used well, these patterns make code easier to reuse, test, and understand. They work best with pure, single-purpose functions and are especially helpful in formatting, validation, and transformation pipelines.

If you want to go further, next study higher-order functions, partial application, and array method chaining so you can recognize when these patterns simplify your code and when a plain function is the better choice.