Learn how to combine small, reusable functions into powerful pipelines for cleaner, faster, and more maintainable code.

Introduction
If you’ve ever written this:
const result = exclaim(toUpperCase(trim(input)));
You’ve already used function composition; you just didn’t call it that.
Function composition is the art of combining smaller functions into bigger ones, where the output of one becomes the input of the next.
It’s like LEGO for your logic, small, reusable bricks forming complex structures.
In this post, we’ll explore 5 practical, real-world examples of function composition in JavaScript, including:
- String formatting
- Array transformations
- API data processing
- Error handling
- Async composition
Let’s make composition go from a theoretical concept to → daily dev habit.
What Is Function Composition?
Definition: Function composition is combining two or more functions to form a new one.
Visually:
f ∘ g = f(g(x))
In JavaScript:
const compose = (f, g) => (x) => f(g(x));
Example:
const trim = (s) => s.trim();
const toUpper = (s) => s.toUpperCase();
const shout = compose(toUpper, trim);
console.log(shout(" hello ")); // "HELLO"
The data flows right to left trim
runs first, then toUpper
.
1. Clean String Formatting
This is the most common use case, turning messy user input into clean, consistent data.
const trim = (s) => s.trim();
const toLower = (s) => s.toLowerCase();
const removeSpaces = (s) => s.replace(/\s+/g, " ");
const compose = (...fns) => (x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
const sanitizeInput = compose(removeSpaces, toLower, trim);
console.log(sanitizeInput(" Hello WORLD ")); // "hello world"
✅ Why it’s useful: Keeps every transformation isolated and testable.
✅ Pattern: Clean → Normalize → Format
2. Data Transformation Pipelines
You can build data processing flows just like, .map().filter().reduce()
but more modular.
const double = (x) => x * 2;
const square = (x) => x ** 2;
const sum = (arr) => arr.reduce((a, b) => a + b, 0);
const compose = (...fns) => (x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
const processNumbers = compose(sum, (arr) => arr.map(square), (arr) => arr.map(double));
console.log(processNumbers([1, 2, 3, 4])); // 120
Flow:
→ Double all numbers → Square them → Sum results
✅ Why it’s useful: Makes your transformations declarative and reusable across multiple pipelines.
3. API Data Processing
Most API responses need cleaning before use. Composition helps you build readable data pipelines.
const pick = (keys) => (obj) =>
Object.fromEntries(Object.entries(obj).filter(([k]) => keys.includes(k)));
const rename = (map) => (obj) =>
Object.fromEntries(Object.entries(obj).map(([k, v]) => [map[k] || k, v]));
const toCamelCase = (str) =>
str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
const camelKeys = (obj) =>
Object.fromEntries(Object.entries(obj).map(([k, v]) => [toCamelCase(k), v]));
const compose = (...fns) => (x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
const transformUser = compose(
rename({ user_name: "username" }),
pick(["id", "user_name", "email"]),
camelKeys
);
const apiResponse = {
user_name: "codeByUmar",
email: "umar@example.com",
created_at: "2025-01-01",
id: 42,
};
console.log(transformUser(apiResponse));
/*
{
id: 42,
username: "codeByUmar",
email: "umar@example.com"
}
*/
✅ Why it’s useful: Converts raw API responses into UI-ready data with readable, chainable logic.
4. Safe Error Handling in Pipelines
Composition helps isolate failure points gracefully.
const tryCatch = (fn, fallback) => (x) => {
try {
return fn(x);
} catch (err) {
console.warn("Error:", err.message);
return fallback;
}
};
const parseJSON = (s) => JSON.parse(s);
const extractName = (obj) => obj.name.toUpperCase();
const safeProcess = compose(
tryCatch(extractName, "Unknown"),
tryCatch(parseJSON, {})
);
console.log(safeProcess('{"name":"Umar"}')); // "UMAR"
console.log(safeProcess("invalid-json")); // "Unknown"
✅ Why it’s useful: Keeps pipelines stable; one broken step doesn’t crash the flow.
5. Async Function Composition (Promises)
You can also compose async functions, perfect for APIs, DB calls, or async workflows.
const pipeAsync = (...fns) => (x) =>
fns.reduce((chain, fn) => chain.then(fn), Promise.resolve(x));
const fetchUser = async (id) =>
({ id, name: "Umar", email: "umar@example.com" });
const toUpper = (s) => s.toUpperCase();
const extractName = (user) => user.name;
const shoutName = async (name) => `HELLO, ${name}!`;
const greetUser = pipeAsync(fetchUser, extractName, toUpper, shoutName);
greetUser(1).then(console.log); // "HELLO, UMAR!"
✅ Why it’s useful: Creates linear async flows instead of messy nested then()
chains.
✅ Pattern: Fetch → Extract → Transform → Use
Bonus: pipe()
vs compose()
compose(f, g, h)
→ runs right to left (like math)pipe(f, g, h)
→ runs left to right (like reading)
const pipe = (...fns) => (x) =>
fns.reduce((acc, fn) => fn(acc), x);
Use whichever fits your mental model; they’re two sides of the same coin.
Why Function Composition Matters
Function composition is the backbone of clean code:
- Makes logic modular and testable
- Eliminates repetitive boilerplate
- Turns imperative chains into declarative flows
- Encourages single-responsibility functions
It’s how libraries like RxJS, Ramda, Lodash/fp, and even React Hooks internally structure data flow.
In short:
Composition isn’t a “functional programming trick.” It’s the secret to writing code that reads like English.
Conclusion
Function composition lets you build complex behaviors from small, predictable functions.
✅ Recap:
- Clean and format strings
- Transform arrays and data
- Shape API responses
- Handle errors safely
- Build async workflows
You’re not writing clever code, you’re writing composable, maintainable, and scalable code.
Call to Action
Have you used function composition in your project, or maybe built your own pipe()
helper?
Drop your examples in the comments 👇
And if your teammate still writes this post, they’ll never look at function calls the same way again.
Leave a Reply