Know about iterators, iterable, generators, and how to use generators in asynchronous programming in JavaScript.
Iterator and Iterable
JavaScript has an iteration protocol which define the iterator protocol and the iterator protocol. These protocols gives a standard way to create sequence of values and iterate over them.
- Iterable — Its an object that has the
Symbol.iterator
method on it. This method returns an iterator object which can be used to iterate over the elements of the object. This is the iterable protocol. Some built-in iterables are Array and Map (but not Object). - Iterator — Its an object that has the
next
method on it which returns an object with the current iteration’svalue
anddone
boolean which tells whether the iteration is done or not. This is the iterator protocol.
We can use the iterable protocol to change or create a behavior for looping over a sequence in a for..of
construct.
There’s also async iteration protocol which has similar interface to work with, but it returns value wrapped in a Promise. The method for iterable is
Symbol.asyncIterator
.
Consider the following example to see the difference between an iterator and an iterable.
// =======================================
// Iterable
// =======================================
var iterableObject = {
items: ["🍒", "🍌", "🍎", "🥝", "🥑"],
[Symbol.iterator]: function () {
var index = 0;
return {
next: () => {
if (index < this.items.length) {
return { value: this.items[index++], done: false };
} else {
index = 0;
return { done: true };
}
},
};
},
};
// Returning an new iterator object each time
console.log(iterableObject[Symbol.iterator]().next()); // { value: '🍒', done: false }
console.log(iterableObject[Symbol.iterator]().next()); // { value: '🍒', done: false }
var iterator = iterableObject[Symbol.iterator]();
console.log(iterator.next()); // { value: '🍒', done: false }
console.log(iterator.next()); // { value: '🍌', done: false }
console.log(iterator.next()); // { value: '🍎', done: false }
console.log(iterator.next()); // { value: '🥝', done: false }
console.log(iterator.next()); // { value: '🥑', done: false }
console.log(iterator.next()); // { done: true }
console.log(iterator.next()); // { value: '🍒', done: false }
for (let item of iterableObject) {
console.log(item);
}
// =======================================
// Iterator
// =======================================
var iteratorObject = {
items: ["🍒", "🍌", "🍎", "🥝", "🥑"],
index: 0,
next: function () {
if (this.index < this.items.length) {
return { value: this.items[this.index++], done: false };
} else {
this.index = 0;
return { done: true };
}
},
};
console.log(iteratorObject.next()); // { value: '🍒', done: false }
console.log(iteratorObject.next()); // { value: '🍌', done: false }
console.log(iteratorObject.next()); // { value: '🍎', done: false }
console.log(iteratorObject.next()); // { value: '🥝', done: false }
console.log(iteratorObject.next()); // { value: '🥑', done: false }
console.log(iteratorObject.next()); // { done: true }
console.log(iteratorObject.next()); // { value: '🍒', done: false }
for (let item of iteratorObject) {
console.log(item);
}
// for (let item of iteratorObject) {
// ^
//
// TypeError: iteratorObject is not iterable
Generator
All normal functions in JavaScript has a run-to-completion semantic. This means that whenever a function starts running, it will always run to the end of that function and finish before any other function comes in and starts running. It can call other functions but no one come in and pre-emptively interrupt this function to run something else.
This is one of the most important characteristics of JavaScript, that its single threaded and only one thing can run at a time.
Generators don’t have run-to-completion semantics. Its a different kind of function. It returns a generator object is a type of iterator (following the iterator and iterable protocols). Only a generator function (function*
) can create a generator object.
Consider the following code example:
function* print() {
console.log(1);
console.log(2);
console.log(3);
yield "pause 1";
console.log(4);
console.log(5);
yield "pause 2";
console.log(6);
console.log(7);
}
// Doesn't runs the generator function
var iterator = print();
console.log(iterator); // Object [Generator] {}
// Start's the generator function and ends up pausing at the first yield
// { value: 'pause 1', done: false }
console.log(iterator.next());
console.log(iterator.next()); // { value: 'pause 2', done: false }
// Resumes from the last yield and completes the exeuction of the function
// and since the function has ended, the done is true and the value is undefined
// as there's no yield statement left
// { value: undefined, done: true }
console.log(iterator.next());
console.log(iterator.next()); // { value: undefined, done: true }
// Output:
// Object [Generator] {}
// 1
// 2
// 3
// { value: 'pause 1', done: false }
// 4
// 5
// { value: 'pause 2', done: false }
// 6
// 7
// { value: undefined, done: true }
// { value: undefined, done: true }
// Won't print anything as the iterator is exhausted
for (let value of iterator) {
console.log(value);
}
The yield
keyword, wherever it shows up, even in between an expression, everything will literally pause. The generator enters this pause state and will remain there indefinitely unless some other actor comes along and says it’s time to resume.
It isn’t blocking the overall blocking, its a localised blocking, only inside the generator.
Another example of using the iterator in a for..of
loop:
function* print() {
console.log(1);
yield "pause 1";
console.log(4);
console.log(5);
yield "pause 2";
console.log(6);
}
var iterator = print();
for (let value of iterator) {
console.log(`value: ${value}`);
}
// Output:
// 1
// value: pause 1
// 4
// 5
// value: pause 2
// 6
Generators are syntactic form of declaring a state machine. State machines are a way of having patterned series of flow from one state to another and declaratively listing all of those states and those transitions out. Implementing state machines without generators is very complex.
Never ending
Generators don’t ever need to be completed. You can partially consume it and when you don’t need, don’t reference it, the garbage collector will remove it.
function* generateUniqueID() {
var id = 0;
// Generally while true loop is not a good idea
// but in this case it is great
while (true) {
yield id++;
}
}
var idGenerator = generateUniqueID();
console.log(idGenerator.next().value); // 0
console.log(idGenerator.next().value); // 1
console.log(idGenerator.next().value); // 2
Async with Generators
We can write 100% of our asynchronous code using generators, and there would be no need to use promises chains.
/**
* Retrieves data after a delay.
* @param {number} number - The number to pass to the run function.
*/
function getData(number) {
setTimeout(function () {
run(number);
}, 1000);
}
/**
* Creates a coroutine from a generator function.
* @param {function} generator - The generator function.
* @returns {function} - The coroutine function.
*/
function coroutine(generator) {
var iterator = generator();
return function () {
return iterator.next.apply(iterator, arguments);
};
}
/**
* Executes a coroutine function that performs asynchronous operations.
* @function run
* @returns {void}
*/
var run = coroutine(function* () {
console.log("Started");
// Parentheses here are required, part of the language's grammar.
var x = 1 + (yield getData(10));
console.log("x: " + x);
var y = 1 + (yield getData(20));
console.log("y: " + y);
var answer = yield getData(x + y);
console.log(answer);
});
run();
// Output:
// Started
// x: 11
// y: 21
// 32
If promises are about solving inversion of control issues in callbacks then generators are about solving the non-local and non-sequential reasonability problem.
Generators make our code look synchronous. We can also do error handling just using try..catch
. In callbacks, thunk and promises, we’ve a lot of nesting, here we don’t have that. That’s huge! The flow control we get is fantastic.
Promises and Generators
In the following example we’ve solved the issue of non-local and non-sequential issue but we’re susceptible to inversion of control issue.
function getData(number) {
setTimeout(function () {
run(number);
}, 1000);
}
function coroutine(generator) {
var iterator = generator();
return function () {
return iterator.next.apply(iterator, arguments);
};
}
var run = coroutine(function* () {
var x = 1 + (yield getData(10));
var y = 1 + (yield getData(20));
var answer = yield getData(x + y);
console.log(answer);
});
run();
Somebody can call iterator.next
and screw up things. This issue is solved by mixing Generators with Promises.
The pattern goes something like this — we yield a promise and the promise resumes the generator. This solves non-sequential non-local issue (via Generators) and it also solves inversion of control issue (via Promises).
function* generator() {
console.log("Started");
// yield a promise
var result = yield Promise.resolve(1);
console.log(result);
// yield another promise
var result = yield Promise.resolve(2);
console.log(result);
console.log("Finished");
}
// start the generator and get the first promise
var iterator = generator();
var promise1 = iterator.next().value;
promise1.then(function (result1) {
// pass the result of the first promise and get the second promise
var promise2 = iterator.next(result1 * 2).value;
promise2.then(function (result2) {
// pass the result of the second promise and resume the generator
iterator.next(result2 * 2);
});
});
// Output:
// Started;
// 2;
// 4;
// Finished;
All of these complexities of using generators and promises is resolved via async..await
. Before them we had to use a library.
Now, there’s a caveat with
async..await
. It’s not just a syntactic sugar. If we want to stop their execution in between, we can’t that. So, they return a promise and not implemented but a proposal, its that promise could be cancellable so that we can cancel the promise which will send the abort message to the async function (bad idea, cancellation is important but this way, it’s a bad design).
Following is a way to add cancellation:
function timeout(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
function makeCancellablePromise(promise) {
var isCancelled = false;
var wrappedPromise = new Promise(function (resolve, reject) {
promise.then(
function (value) {
isCancelled ? reject({ isCancelled, value }) : resolve(value);
},
function (error) {
isCancelled ? reject({ isCancelled, error }) : reject(error);
}
);
});
return {
promise: wrappedPromise,
cancel() {
isCancelled = true;
},
};
}
async function main() {
var { promise, cancel } = makeCancellablePromise(timeout(2000));
setTimeout(() => cancel(), 1500); // cancel the promise after 1.5 seconds
try {
await promise;
console.log("world");
} catch (error) {
if (error.isCancelled) {
console.log("Operation was cancelled");
} else {
console.log("An error occurred:", error);
}
}
}
main();
A better solution would be adding a race condition between timeout and async task:
async function timeout(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
async function runWithTimeout(fn, timeoutDuration) {
return await Promise.race([
fn(),
new Promise(function (_, reject) {
setTimeout(
() => reject(new Error("Operation timed out")),
timeoutDuration
);
}),
]);
}
async function main() {
await timeout(1000); // wait for 1 second
console.log("hello");
var result = await runWithTimeout(() => timeout(2000), 1500);
if (result instanceof Error) {
console.log(result.message);
} else {
console.log("world");
}
}
main();
Generators don’t suffer this issue. They return iterators which have next
method on them. If we want to cancel by using the return
method or throw
method which can manually send an error to the generator.
Generators give us more control from the outside than the async function gives you.
Generators are also used in CSP async pattern.
Usage of Generators with Promises for Async
Consider a problem where we’re in a restaurant and have ordered our starter, main course, and dessert. They all should be prepared concurrently but served synchronously.
Initial code:
/**
* Serves a meal based on the specified meal type.
* @param {string} mealType - The type of meal to serve (starter, main, dessert).
* @param {function} cb - The callback function to be called after the meal is prepared.
*/
function serve(mealType, cb) {
var meal = {
starter: "🍤",
main: "🥘",
dessert: "🍨",
};
var delay = (Math.round(Math.random() * 1e4) % 8000) + 1000;
console.log(`Preparing ${mealType}`);
setTimeout(function () {
cb(meal[mealType]);
}, delay);
}
/**
* Makes food of the specified meal type.
* @param {string} mealType - The type of meal to make.
*/
function makeFood(mealType) {
serve(mealType, function (food) {
// ...
});
}
// Make food concurrently
makeFood("starter");
makeFood("main");
makeFood("dessert");
Running them concurrently and coordinating between them bring complexities. Solving this complexity using generators and promises.
Solution:
/**
* Serves a meal based on the specified meal type.
* @param {string} mealType - The type of meal to serve (starter, main, dessert).
* @param {function} cb - The callback function to be called after the meal is prepared.
*/
function serve(mealType, cb) {
var meal = {
starter: "🍤",
main: "🥘",
dessert: "🍨",
};
var delay = (Math.round(Math.random() * 1e4) % 8000) + 1000;
console.log(`Preparing ${mealType}`);
setTimeout(function () {
cb(meal[mealType]);
}, delay);
}
/**
* Makes food of the specified meal type.
* @param {string} mealType - The type of meal to make.
*/
function makeFood(mealType) {
return new Promise(function executor(resolve) {
serve(mealType, function (food) {
resolve(food);
});
});
}
function* main() {
// Make food concurrently
var promise1 = makeFood("starter");
var promise2 = makeFood("main");
var promise3 = makeFood("dessert");
console.log(`Served ${yield promise1}`);
console.log(`Served ${yield promise2}`);
console.log(`Served ${yield promise3}`);
}
var iterator = main();
iterator
.next()
.value.then(function (starter) {
return iterator.next(starter).value;
})
.then(function (main) {
return iterator.next(main).value;
})
.then(function (dessert) {
iterator.next(dessert);
});
// Output:
// Preparing starter
// Preparing main
// Preparing dessert
// Served 🍤
// Served 🥘
// Served 🍨
Since, here we’re yielding promises we can yield Promise.all
or any other abstraction we want. That’s why this pattern is so powerful.