Problem
flow silently breaks when any composed function returns a Promise. It passes the raw Promise object to the next function instead of the resolved value:
const fetchUser = async (id: number) => db.users.find(id);
const getName = (user: User) => user.name;
const getUserName = flow(fetchUser, getName);
await getUserName(1); // TypeError: Cannot read property 'name' of [object Promise]
The implementation has no await:
// current flow implementation
for (let i = 1; i < funcs.length; i++) {
result = funcs[i].call(this, result); // passes raw Promise
}
Proposal
const getUserName = flowAsync(fetchUser, getName);
await getUserName(1); // "Alice"
Implementation is flow + async + two awaits:
export function flowAsync(...funcs) {
return async function (this: any, ...args: any[]) {
let result = funcs.length ? await funcs[0].apply(this, args) : args[0];
for (let i = 1; i < funcs.length; i++) {
result = await funcs[i].call(this, result);
}
return result;
};
}
Precedent
es-toolkit already follows a sync/Async naming convention:
attempt / attemptAsync
filter / filterAsync
flatMap / flatMapAsync
map / mapAsync
reduce / reduceAsync
flow / flowAsync fits this pattern exactly.
The TC39 proposal-function-pipe-flow included Function.flowAsync but was withdrawn at Stage 1, with the committee noting these are "easily solved by userland functions" — which is what es-toolkit provides.
Problem
flowsilently breaks when any composed function returns aPromise. It passes the rawPromiseobject to the next function instead of the resolved value:The implementation has no
await:Proposal
Implementation is
flow+async+ twoawaits:Precedent
es-toolkit already follows a
sync/Asyncnaming convention:attempt/attemptAsyncfilter/filterAsyncflatMap/flatMapAsyncmap/mapAsyncreduce/reduceAsyncflow/flowAsyncfits this pattern exactly.The TC39
proposal-function-pipe-flowincludedFunction.flowAsyncbut was withdrawn at Stage 1, with the committee noting these are "easily solved by userland functions" — which is what es-toolkit provides.