composeAsync
Prompt
Implement a composeAsync function that takes any number of functions (sync or async) and returns a new function. When the returned function is called with a value, it applies the given functions right to left, awaiting each step before passing the result to the next.
If no functions are passed, the returned function should resolve with its input unchanged.
Requirements
- Takes any number of functions and returns a new function
- The returned function takes one value and returns a promise
- Functions run from right to left; each one receives the resolved value of the function to its right
- Works for a mix of sync and async functions
- With no functions, the returned function resolves with its input unchanged
- If any function throws or its returned promise rejects, the composed promise rejects
Playground
composeAsync is the async version of compose. Same
right-to-left order. The only new thing is that each step
must be awaited before the next one runs.
Walk the array from right to left with a for loop and
await each function call. Or use reduceRight seeded
with Promise.resolve(value) and chain .then(fn) at
each step.
You do not need a try/catch. await rethrows rejected
promises, and .then passes rejections through. A single
failing step will reject the composed promise on its own.
Solution
Explanation
The idea is the same as compose: run functions right to left so that composeAsync(f, g, h)(x) reads like f(g(h(x))). The only difference is that each function can return a promise, and we need to wait for it before feeding the result to the next one.
The code
function composeAsync(...fns) { return async function (value) { let result = value; for (let i = fns.length - 1; i >= 0; i--) { result = await fns[i](result); } return result; };}composeAsync collects all functions with rest parameters, then returns a new async function. That function walks the array backward, calls each function with the current result, and awaits it. The final result is returned, so the outer function resolves with it.
If no functions are passed, the for loop never runs and result is returned as is. That matches the identity behavior we want.
Why await handles sync functions too
If a function returns a plain value instead of a promise, await wraps it in a resolved promise. So mixing double (sync) and addOneAsync (async) in the same composition works without special handling.
const double = (x) => x * 2;const addOneAsync = async (x) => x + 1;composeAsync(addOneAsync, double)(5).then(console.log); // 11double(5) returns 10. await 10 gives 10. Then addOneAsync(10) returns a promise resolving to 11. await waits for it. Final value: 11.
Error propagation comes for free
If any step throws or rejects, await rethrows the error. The for loop exits, the async function returns a rejected promise, and the caller's .catch handles it.
composeAsync(async () => { throw new Error('boom');})(0).catch((e) => console.log(e.message)); // boomNo try/catch needed inside composeAsync.
Alternative with reduceRight
If you prefer reduceRight, seed it with Promise.resolve(value) and chain .then(fn) at each step:
function composeAsync(...fns) { return function (value) { return fns.reduceRight( (promise, fn) => promise.then(fn), Promise.resolve(value) ); };}Both work. The for loop reads top-to-bottom and is simple to step through in a debugger. reduceRight is shorter.
A common mistake is to seed the reduce with value
instead of Promise.resolve(value). If the rightmost
function is sync and returns a plain value, .then does
not exist on it and the call throws. Seeding with
Promise.resolve(value) keeps the chain uniformly a
promise at every step.
Libraries like Ramda ship composeP for this exact
pattern. Redux Saga and RxJS use pipe-style composition
for their operators. Knowing composeAsync is handy any
time you are chaining steps in a data transform where each
step may do I/O.