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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions