Repeaters catch and propagate any errors they receive in a predictable, well-specified way — important for building robust applications. Every promise passed to a repeater is caught preemptively to prevent unhandled rejections.

The four ways a repeater can error

1. Calling stop with an error

The most common way to cause a repeater to error is to call the stop function with the error.

const repeater = new Repeater((push, stop) => {
  for (let i = 0; i < 100; i++) {
    push(i);
  }

  stop(new Error("Stop in the name of love 😘"));
});

(async function() {
  try {
    console.log(await repeater.next()); // { value: 0, done: true }
    console.log(await repeater.next()); // { value: 1, done: true }
    console.log(await repeater.next()); // { value: 2, done: true }
    console.log(await repeater.return()); // This line throws an error.
  } catch (err) {
    console.log(err); // Error: Stop in the name of love 😘
  } finally {
    console.log(await repeater.next()); // { done: true }
    console.log(await repeater.next()); // { done: true }
  }
})();

When stop is called with an error, values which were previously pushed can continue to be pulled. After all previously pushed values are exhausted, the final call to next rejects with the error. If the repeater is prematurely finished using Repeater.prototype.return, the repeater drops any remaining values and rejects the final iteration with the error.

As seen in the example above, repeaters error only once before entering a finished state where all calls to next resolve to { done: true }. This mirrors the finishing behavior of async generator objects. Only the first call to stop has any effect on the repeater, and any errors passed to stop in subsequent calls are dropped.

2. A promise passed to the push function rejects

const repeater = new Repeater(async (push, stop) => {
  await push("a");
  await push("b");
  await push("c");
  await push(new Promise((_, reject) => {
    setTimeout(() => reject(new Error("A rejection passed to push ⏰")), 100);
  }));
  // these values are dropped
  await push("e");
  await push("f");
  // this error is ignored
  stop(new Error("Stop in the name of love 😘"));
});

(async function() {
  try {
    for await (const letter of repeater) {
      console.log(letter); // "a", "b", "c"
    }
  } catch (err) {
    console.log(err); // Error: A rejection passed to push ⏰
  } finally {
    console.log(await repeater.next()); // { done: true }
  }
})();

Repeaters unwrap promises passed to push before handing them to consumers. If a pushed promise rejects, the repeater finishes and drops any values pushed after it, no matter when they settle. A rejection that settles before the repeater stops rejects the final iteration; one that settles after is dropped. This behavior is useful when creating inverted repeaters.

3. The executor throws an error

Repeaters catch both synchronous and asynchronous errors thrown by the executor.

const repeater = new Repeater((push, stop) => {
  push("a");
  push("b");
  push("c");
  // this error is dropped
  stop(new Error("My error"));
  // this error takes priority
  throw new Error("This executor is busted ☠️");
});

(async function() {
  try {
    for await (const letter of repeater) {
      console.log(letter); // "a", "b", "c"
    }
  } catch (err) {
    console.log(err); // Error: This executor is busted ☠️
  } finally {
    console.log(await repeater.next()); // { done: true }
  }
})();

When the executor throws, the repeater stops and the final iteration rejects with the error. Because executor errors usually signal a bug, they take precedence over all other errors passed to the repeater.

4. Calling the throw method

The async iterator interface defines an optional throw method which allows consumers to throw errors into the iterator. With async generators, the throw method resumes the generator and throws the error at the point of the suspended yield operator. Generators can recover from these errors by wrapping yield operations in a try block.

Repeaters simulate this behavior by causing the promise returned from the previous push call to reject.

const repeater = Repeater(async (push) => {
  for (let i = 0; i < 10; i++) {
    try {
      await push(i);
    } catch (err) {
      console.log(err);
      console.log("Hello I caught your error 👀");
    }
  }
});

(async function() {
  console.log(await repeater.next()); // { value: 1, done: false };
  console.log(await repeater.throw(new Error("Hello? 📞"))); 
  // Error: Hello? 📞
  // Hello I caught your error 👀
  // { value: 2, done: false }
  console.log(await repeater.next()); // { value: 3, done: false };
  console.log(await repeater.next()); // { value: 4, done: false };
})();

If you ignore the promise returned from push — you don’t await it and never call its then/catch — the throw method simply rethrows the error. This makes it safe to fire-and-forget push. But once you await (or otherwise use) the push promise, handling and propagating errors from throw becomes your responsibility.

Note: The throw method also rethrows its error immediately when there is no pending push for it to reject — that is, when the repeater: