Skip to the code
cf.

Run a generator as a coroutine

Before async functions, libraries like co let you write async code with generators: the generator yields a promise, and a runner resumes it with the promise's value once it's ready. Write that runner, run(genFn, ...args).

run calls genFn(...args) to get a generator and starts it. Each time the generator yields a value, run waits for it and resumes the generator with the result, so const user = yield fetchUser() gets the user. Promises and thenables are waited for; any other value is passed straight back.

When a yielded promise rejects, run throws the reason into the generator at that yield, so the generator can catch it with try...catch.

run returns a promise. It fulfills with the generator's return value, or rejects with any error the generator throws and doesn't catch, including one thrown before its first yield. run itself never throws.

const wait = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
 
const total = await run(function* (a) {
  const b = yield wait(100, 2); // resumes 100 ms later with 2
  const c = yield 3;            // not a promise: resumes with 3
  return a + b + c;
}, 1);
// total is 6
 
const message = await run(function* () {
  try {
    yield Promise.reject(new Error("offline"));
  } catch (error) {
    return "handled " + error.message;
  }
});
// message is "handled offline"

What the tests check

  1. resumes the generator with each promise's value
    const result = await run(function* () {
      const a = yield Promise.resolve(2);
      const b = yield Promise.resolve(3);
      return a * b;
    });
    expect(result).toBe(6);
    
  2. waits for each promise before resuming
    const clock = useFakeTimers();
    const wait = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
    const log = [];
    const done = run(function* () {
      log.push("start");
      const a = yield wait(100, "a");
      log.push(a);
      const b = yield wait(50, "b");
      log.push(b);
      return log.length;
    });
    await clock.tickAsync(99);
    expect(log).toEqual(["start"]);
    await clock.tickAsync(1);
    expect(log).toEqual(["start", "a"]);
    await clock.tickAsync(50);
    expect(log).toEqual(["start", "a", "b"]);
    await expect(done).resolves.toBe(3);
    
  3. passes values that aren't promises straight back
    const result = await run(function* () {
      const x = yield 5;
      const y = yield "text";
      const z = yield null;
      return [x, y, z];
    });
    expect(result).toEqual([5, "text", null]);
    
  4. waits for thenables too
    const thenable = {
      then(resolve) {
        resolve(42);
      },
    };
    await expect(run(function* () {
      const value = yield thenable;
      return value + 1;
    })).resolves.toBe(43);
    
  5. throws a rejection into the generator, where try...catch can handle it
    const error = new Error("offline");
    const result = await run(function* () {
      try {
        yield Promise.reject(error);
        return "not caught";
      } catch (caught) {
        const retried = yield Promise.resolve("retried");
        return caught === error ? "caught, then " + retried : "caught something else";
      }
    });
    expect(result).toBe("caught, then retried");
    
  6. rejects with an error the generator doesn't catch
    const error = new Error("offline");
    await expect(run(function* () {
      yield Promise.reject(error);
    })).rejects.toBe(error);
    await expect(run(function* () {
      yield 1;
      throw new TypeError("bad input");
    })).rejects.toThrow(TypeError);
    
  7. returns a rejected promise, rather than throwing, when the generator fails at once
    let promise;
    expect(() => {
      promise = run(function* () {
        throw new RangeError("too soon");
      });
    }).not.toThrow();
    await expect(promise).rejects.toThrow(RangeError);
    
  8. passes the extra arguments to genFn
    await expect(run(function* (a, b) {
      return a + b;
    }, 2, 3)).resolves.toBe(5);
    

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