Skip to the code
cf.

Async pipe

Write asyncPipe(...fns). It returns a function that passes its input through each function in turn, left to right, and returns a promise of the final result. Each function may return a plain value or a promise; wait for it to fulfill, then call the next function with the value.

The first function gets every argument the piped function was called with; the others get one value each. With no functions, the result is the first argument. The piped function always returns a promise, even when every step is synchronous. If a step throws or rejects, that promise rejects with the error, and the steps after it don't run.

const getUserName = asyncPipe(
  (id) => fetchJson("/api/users/" + id), // returns a promise
  (user) => user.name,                    // returns a plain value
  (name) => name.toUpperCase(),
);
 
await getUserName(42); // "ADA"

What the tests check

  1. passes the value through each function, left to right
    const run = asyncPipe(
      (x) => x + 1,
      (x) => x * 2,
      (x) => x - 3,
    );
    await expect(run(5)).resolves.toBe(9);
    
  2. waits for each promise before calling the next function
    const clock = useFakeTimers();
    const slowIncrement = (x) => new Promise((resolve) => setTimeout(resolve, 100, x + 1));
    const run = asyncPipe(slowIncrement, (x) => x * 10);
    let result;
    run(1).then((value) => {
      result = value;
    });
    await clock.tickAsync(100);
    expect(result).toBe(20);
    
  3. always returns a promise
    const promise = asyncPipe((x) => x + 1)(1);
    expect(promise).toBeInstanceOf(Promise);
    await expect(promise).resolves.toBe(2);
    
  4. passes every argument to the first function
    const run = asyncPipe(
      (a, b) => a + b,
      (sum) => sum * 10,
    );
    await expect(run(2, 3)).resolves.toBe(50);
    
  5. resolves with the input when there are no functions
    await expect(asyncPipe()("same")).resolves.toBe("same");
    
  6. rejects with the error and skips the steps after it
    const error = new Error("step 2 failed");
    const later = spy();
    const run = asyncPipe(
      (x) => x,
      async () => {
        throw error;
      },
      later,
    );
    await expect(run(1)).rejects.toBe(error);
    expect(later).not.toHaveBeenCalled();
    
  7. rejects instead of throwing when a step throws
    const error = new Error("bad input");
    const run = asyncPipe(() => {
      throw error;
    });
    let promise;
    expect(() => {
      promise = run(1);
    }).not.toThrow();
    await expect(promise).rejects.toBe(error);
    
  8. can be called again and again
    const run = asyncPipe(
      (x) => x + 1,
      (x) => x * 2,
    );
    await expect(run(1)).resolves.toBe(4);
    await expect(run(2)).resolves.toBe(6);
    await expect(run(3)).resolves.toBe(8);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).