composeAsync

Medium20 minAdobe

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

Hint 1

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.

Hint 2

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.

Hint 3

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); // 11

double(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)); // boom

No 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.