An iterator hands out values one at a time: each call to its next() method returns an object like { value: 1, done: false }, until one says done: true. for...of, spread, and destructuring all read values by calling next(). Often the code reading stops before the end, such as a loop that breaks once it finds what it wanted. Closing the iterator means telling it that no more values will be asked for, so it can clean up: stop a timer, close a connection, release a lock. The iteration protocol, the shape every iterator follows, has an optional method for that: return().
It's like telling a waiter you've had enough before the last course: the kitchen stops cooking for you and clears up, instead of keeping your table laid all night.
function* lines() {
console.log("open");
try {
yield "first";
yield "second";
yield "third";
} finally {
console.log("close"); // runs however the generator ends
}
}
for (const line of lines()) {
console.log(line);
if (line === "second") break;
}
// logs open, first, second, close: break called return(), which ran the finally block
const [first] = lines();
// logs open, close: destructuring read one value, then closed the generator
const manual = lines();
manual.next(); // logs open; { value: "first", done: false }
// nothing closes manual for you: it stays paused at its first yield
manual.return("stopped"); // logs close; { value: "stopped", done: true }
manual.next(); // { value: undefined, done: true }
| When the reading stops | Is return() called? |
|---|---|
for...of ends early: break, return, or an exception thrown in the loop body | Yes |
for await...of ends early in the same ways | Yes, on the async iterator, and the loop waits for the promise it returns |
Destructuring has what it needs before the iterator says done, as const [a, b] = iterator does after two values | Yes |
The iterator says done: true, as at the end of every [...iterator] | No: it has already finished |
The iterator's own next() throws | No: the iterator failed, so it's treated as finished |
Your code calls next() by hand and stops calling it | No, unless you call return() yourself |
On a generator, return() resumes it at the yield where it's paused, as if that yield were a return statement. The generator ends there, but its finally blocks run on the way out, so cleanup written in a finally happens however the generator stops. A generator that hasn't started, because next() was never called, finishes at once: its body never runs, so it has nothing to clean up. Calling return() on a generator that has already finished changes nothing.
return() is optional. Array, string, Map, and Set iterators don't have one, since they hold nothing that needs releasing: [1, 2][Symbol.iterator]().return is undefined. So code that closes an iterator calls return() only when it exists. Generators, sync and async, always have one, and an iterator you write by hand can have one too.
You rely on closing whenever a generator holds something open: a loop over it can break early, and the generator still tidies up. Code that reads from an iterator and stops before the end is expected to do what for...of does, and close it.
Write merge(...sources). Each source is an async iterable, such as an async generator. merge returns an async iterable that yields the values of all the sources as they arrive, whichever source they come from: if the second source produces a value before the first, that value comes out first.
Every value comes out exactly once, and the merged iterable ends once every source is done. With no sources, it ends at once.
If a source throws, the merged iteration throws that error.
When the merged iteration ends early, because the caller stopped (with break, or by calling return()) or because a source threw, it closes every source that's still running: it calls their return() method, if they have one.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* ticks(name, every, count) {
for (let i = 1; i <= count; i++) {
await wait(every);
yield name + i;
}
}
for await (const value of merge(ticks("a", 30, 3), ticks("b", 50, 2))) {
console.log(value);
}
// logs a1 (at 30 ms), b1 (50 ms), a2 (60 ms), a3 (90 ms), b2 (100 ms)
const clock = useFakeTimers();
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* ticks(name, every, count) {
for (let i = 1; i <= count; i++) {
await wait(every);
yield name + i;
}
}
const seen = [];
let failure;
(async () => {
for await (const value of merge(ticks("a", 30, 3), ticks("b", 50, 2))) seen.push(value);
})().catch((error) => {
failure = error;
});
await clock.tickAsync(100);
expect(failure).toBeUndefined();
expect(seen).toEqual(["a1", "b1", "a2", "a3", "b2"]);
const clock = useFakeTimers();
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* ticks(name, every, count) {
for (let i = 1; i <= count; i++) {
await wait(every);
yield name + i;
}
}
const seen = [];
let failure;
(async () => {
for await (const value of merge(ticks("a", 30, 2), ticks("b", 50, 1))) seen.push(value);
})().catch((error) => {
failure = error;
});
await clock.tickAsync(30);
expect(seen).toEqual(["a1"]);
await clock.tickAsync(20);
expect(seen).toEqual(["a1", "b1"]);
await clock.tickAsync(10);
expect(seen).toEqual(["a1", "b1", "a2"]);
expect(failure).toBeUndefined();
const clock = useFakeTimers();
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* ticks(name, every, count) {
for (let i = 1; i <= count; i++) {
await wait(every);
yield name + i;
}
}
const seen = [];
let finished = false;
let failure;
(async () => {
for await (const value of merge(ticks("a", 10, 1), ticks("b", 30, 1))) seen.push(value);
finished = true;
})().catch((error) => {
failure = error;
});
await clock.tickAsync(10);
expect(seen).toEqual(["a1"]);
expect(finished).toBe(false);
await clock.tickAsync(20);
expect(seen).toEqual(["a1", "b1"]);
expect(finished).toBe(true);
expect(failure).toBeUndefined();
let empty;
merge()[Symbol.asyncIterator]()
.next()
.then(({ value, done }) => {
empty = { value, done };
});
await flush();
expect(empty).toEqual({ value: undefined, done: true });
const clock = useFakeTimers();
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* ticks(name, every, count) {
for (let i = 1; i <= count; i++) {
await wait(every);
yield name + i;
}
}
const seen = [];
let failure;
(async () => {
for await (const value of merge(ticks("a", 20, 3), ticks("b", 20, 3), ticks("c", 30, 2))) seen.push(value);
})().catch((error) => {
failure = error;
});
await clock.tickAsync(100);
expect(failure).toBeUndefined();
expect(seen).toHaveLength(8);
expect([...seen].sort()).toEqual(["a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2"]);
const clock = useFakeTimers();
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* ticks(name, every, count) {
for (let i = 1; i <= count; i++) {
await wait(every);
yield name + i;
}
}
async function* failing() {
await wait(40);
throw new Error("source failed");
}
const seen = [];
let failure;
(async () => {
for await (const value of merge(ticks("a", 30, 3), failing())) seen.push(value);
})().catch((error) => {
failure = error;
});
await clock.tickAsync(100);
expect(seen).toEqual(["a1"]);
expect(failure).toBeInstanceOf(Error);
expect(failure.message).toBe("source failed");
const clock = useFakeTimers();
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const closed = [];
async function* endless(name, every) {
try {
for (let i = 1; ; i++) {
await wait(every);
yield name + i;
}
} finally {
closed.push(name);
}
}
async function* failing() {
await wait(40);
throw new Error("source failed");
}
const seen = [];
let failure;
(async () => {
for await (const value of merge(endless("a", 30), endless("b", 25), failing())) seen.push(value);
})().catch((error) => {
failure = error;
});
await clock.tickAsync(200);
expect(seen).toEqual(["b1", "a1"]);
expect(failure).toBeInstanceOf(Error);
expect(closed.sort()).toEqual(["a", "b"]);
const clock = useFakeTimers();
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const closed = [];
async function* endless(name, every) {
try {
for (let i = 1; ; i++) {
await wait(every);
yield name + i;
}
} finally {
closed.push(name);
}
}
const seen = [];
let failure;
(async () => {
for await (const value of merge(endless("a", 30), endless("b", 50))) {
seen.push(value);
break;
}
})().catch((error) => {
failure = error;
});
await clock.tickAsync(200);
expect(failure).toBeUndefined();
expect(seen).toEqual(["a1"]);
expect(closed.sort()).toEqual(["a", "b"]);
const clock = useFakeTimers();
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const counter = (name, every, count) => ({
[Symbol.asyncIterator]() {
let i = 0;
return {
async next() {
if (i >= count) return { value: undefined, done: true };
await wait(every);
i += 1;
return { value: name + i, done: false };
},
};
},
});
const all = [];
const firstOnly = [];
let failure;
(async () => {
for await (const value of merge(counter("x", 10, 2), counter("y", 15, 1))) all.push(value);
for await (const value of merge(counter("x", 10, 2), counter("y", 15, 1))) {
firstOnly.push(value);
break;
}
})().catch((error) => {
failure = error;
});
await clock.tickAsync(100);
expect(failure).toBeUndefined();
expect(all).toEqual(["x1", "y1", "x2"]);
expect(firstOnly).toEqual(["x1"]);
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).
I'm here to help with Merge async iterables. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.