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.
- Currying changes a function so it accepts one argument at a time and returns another function until all arguments are provided.
- Composition combines several functions into one pipeline, where each function feeds the next.
- Both techniques encourage small functions that do one job well.
- They are often used together in reusable data-processing code.
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:
- build data transformation helpers
- want to reuse a function with different settings
- need predictable pipelines for validation or formatting
- prefer pure functions that avoid hidden state
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); // 5Here, 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]")); // trueThe 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
- Formatting numbers, dates, or strings with fixed settings.
- Building validation helpers that can be reused across forms or API responses.
- Creating array predicates such as “greater than,” “starts with,” or “matches pattern.”
- Assembling data pipelines for normalization, sanitizing, or mapping.
- Reducing duplication in utility modules and shared helper libraries.
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)); // 24The 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
- Curried functions are not always more readable than regular functions, especially when the logic is short and direct.
- Composition assumes each step returns the right shape for the next step; one incompatible return value can break the chain.
- Many built-in JavaScript methods, such as map and filter, already support a composition-friendly style because they accept one callback at a time.
- If a function needs multiple unrelated inputs, forcing currying may make the API awkward.
- Debugging deeply nested function-returning-function chains can be harder than stepping through plain imperative code.
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
- Currying turns a multi-argument function into a chain of single-argument functions.
- Composition connects functions so the output of one becomes the input of the next.
- Curried functions are useful for specialization and reuse.
- Composed functions are useful for predictable transformation pipelines.
- Both patterns are most effective with small, pure functions.
11. Practice Exercise
Create a curried function and a composed pipeline for a simple profile formatter.
- Write a curried addPrefix function that accepts a prefix and then a string.
- Write a pipe helper that runs functions from left to right.
- Use the helpers to format the string " jane doe " into "User: Jane Doe".
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.