Skip to content

Feature request: flowAsync #1658

@zrosenbauer

Description

@zrosenbauer

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions