Skip to the code
cf.

Run async tasks in series

Write runInSeries(tasks). tasks is an array of functions; each returns a promise or a plain value. Call them one at a time: start each task only after the previous one has finished. Return a promise that fulfills with their results, in order.

If a task fails (its promise rejects, or it throws), reject with that error and don't start the tasks after it. With no tasks, fulfill with [].

const [user, posts] = await runInSeries([
  () => fetchJson("/api/user"),
  () => fetchJson("/api/posts"), // starts only once the user has arrived
]);

What the tests check

  1. fulfills with the results in order
    const clock = useFakeTimers();
    const after = (ms, value) => () => new Promise((resolve) => setTimeout(resolve, ms, value));
    let result;
    runInSeries([after(300, "a"), after(100, "b"), after(200, "c")]).then((value) => {
      result = value;
    });
    await clock.tickAsync(600);
    expect(result).toEqual(["a", "b", "c"]);
    
  2. starts each task only after the previous one finishes
    const clock = useFakeTimers();
    const start = clock.now();
    const startedAt = [];
    const task = (value) => () => {
      startedAt.push(clock.now() - start);
      return new Promise((resolve) => setTimeout(resolve, 100, value));
    };
    runInSeries([task(1), task(2), task(3)]);
    await clock.tickAsync(300);
    expect(startedAt).toEqual([0, 100, 200]);
    
  3. accepts tasks that return plain values
    await expect(runInSeries([() => 1, () => "two", () => null])).resolves.toEqual([1, "two", null]);
    
  4. rejects with the error of the task that fails
    const clock = useFakeTimers();
    const error = new Error("offline");
    let reason;
    runInSeries([
      () => Promise.resolve(1),
      () => new Promise((_, reject) => setTimeout(reject, 100, error)),
      () => 3,
    ]).catch((e) => {
      reason = e;
    });
    await clock.tickAsync(100);
    expect(reason).toBe(error);
    
  5. doesn't start the tasks after one that fails
    const last = spy(() => "never");
    await runInSeries([() => 1, () => Promise.reject(new Error("offline")), last]).catch(() => {}); // it rejects; this test only checks what ran
    await flush();
    expect(last).not.toHaveBeenCalled();
    
  6. rejects when a task throws
    const error = new Error("bad task");
    const failing = () => {
      throw error;
    };
    await expect(runInSeries([() => 1, failing])).rejects.toBe(error);
    
  7. fulfills with [] when there are no tasks
    await expect(runInSeries([])).resolves.toEqual([]);
    

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