Meet the JavaScript pattern of the year or how to handle async like a boss
Sometimes when you learn something new you get really excited. Excited to that level so you want to teach it to someone. That is the case with the concept which I found a couple of months ago. It is an implementation of the command pattern using generators. Or the well known saga used in the redux-saga library. In this article we will see how the idea makes our asynchronous code simpler and easy to read. We will also implement it ourself using generators.
If you are lazy and don’t want to ready everything check the banica repo. It is all the stuff that we say here but wrapped in a library.
Quick introduction to generators
A generator is an object that conforms iterable and iterator protocols. Which means that it is an object that has a Symbol.iterator
key responding to a function returning an iterator. And the iterator defines a standard way to produce values. Every string in JavaScript for example has this characteristics. For example:
const str = 'hello';
const iterator = str[Symbol.iterator]();
iterator.next(); // {value: "h", done: false}
iterator.next(); // {value: "e", done: false}
iterator.next(); // {value: "l", done: false}
iterator.next(); // {value: "l", done: false}
iterator.next(); // {value: "o", done: false}
iterator.next(); // {value: undefined, done: true}
By calling the iterator’s next
method we are asking for the next value in a sequence of values. It is the same with generators. Once we initialize a generator we may fetch values from it.
To define a generator we use a special type of function syntax containing an asterisk before the name of the function and after the function
keyword.
function* createGenerator() {
// ...
}
const generator = createGenerator();
generator.next(); // {value: undefined, done: true}
Once we call that function we receive a generator object with an iterator-ish API. We also have a return
method that completes the generator with a given value and throw
which resumes the generator and throws an error inside.
The most exciting part in the generators is done via the yield
keyword. The yield
expression allows us to pause the function and gives control to the code that runs the generator. Later when we call next
we resume the generator with an optional input. Here is an example:
function* formatAnswer() {
const answer = yield 'foo';
return `The answer is ${ answer }`;
}
const generator = formatAnswer();
console.log(generator.next()); // {value: "foo", done: false}
console.log(generator.next(42)); // {value: "The answer is 42", done: true}
The first next
pauses the function just before assigning a value to the answer
constant. We receive foo
as a value
and done
is false
because the generator is not finished yet. The followed next
call resumes the function with 42
which gets assigned to answer
. And because we have a return
statement the generator is completed and we have done
set to true
.
This type of communication between the generator and the code that iterates it is what we will be using for implementing the command pattern. That is what excited me the most because we are able to handle asynchronous processes by writing code that looks synchronous.
How’s the command pattern looks like
The big deal using the command pattern is to split the code that wants to do something from the code that is actually doing it. Let’s take the following example:
const player = function (name) {
return {
moveLeft() {
console.log(`${ name } moved to the left`);
},
moveRight() {
console.log(`${ name } moved to the right`);
},
jump() {
console.log(`${ name } jumped`);
}
}
}
const p = player('Foo');
p.moveLeft(); // Foo moved to the left
p.jump(); // Foo jumped
We see how the code that wants to make the player jumping is actually doing it (p.jump()
call). That is fine but we may use another implementation:
const player = function (name) {
const commands = {
moveLeft() {
console.log(`${ name } moved to the left`);
},
moveRight() {
console.log(`${ name } moved to the right`);
},
jump() {
console.log(`${ name } jumped`);
}
}
return {
execute(command) {
commands[command.action]();
}
}
}
const p = player('Foo');
p.execute({ action: 'moveLeft' }); // Foo moved to the left
p.execute({ action: 'jump' }); // Foo jumped
We see how that new implementation introduces one more level of abstraction. Now the code that wants to make the player move/jump is not actually doing it. This helps a lot if we have to change the API of the player
. Like for example if we want to rename moveLeft
to moveBackward
and moveRight
to moveForward
. We don’t have to amend all the places which are using these methods but only create an alias in the execute
function. Having such separation also helps us inject logic before the actual method invocation. And if that method is an asynchronous operation we may simply handle it at this level.
Doing the same but using a generator
Let’s keep the idea of having a player
that we need to move and jump. We also want to provide command objects like { action: 'jump' }
and someone else handle the actual work.
function iterateOverTheGenerator(gen, name) {
const status = gen.next();
if (status.done) return;
switch (status.value.action) {
case 'moveLeft': console.log(`${ name } moved to the left`); break;;
case 'moveRight': console.log(`${ name } moved to the right`); break;
case 'jump': console.log(`${ name } jumped`); break;
}
return iterateOverTheGenerator(gen, name);
}
function* createGenerator() {
yield { action: 'moveLeft' };
yield { action: 'jump' };
}
const generator = createGenerator();
/*
It prints:
Foo moved to the left
Foo jumped
*/
iterateOverTheGenerator(generator, 'Foo');
Very often when working with a generator we have a helper that loops over the produced values. Remember how the generator object is actually an iterator. What happens when calling next
is that the function pauses at the first yield
expression and the value
in the { done: <boolean>, value: <something> }
object is what is yield
ed. In our example this is the command object. We see what’s the desired action and call again iterateOverTheGenerator
so we could fetch another instruction. The process continues till we reach the end of the generator (done
is true
).
Of course iterateOverTheGenerator
is really specific and it knows a lot about what kind of commands we want to execute. The goal in this article is to produce a more robust utility that accepts a generator, iterates over its values and execute functions.
Implementing the robust command pattern
More or less the commands that we want to handle outside of the generator are:
- A synchronous function call
- A synchronous function call that returns a promise (that is a function like fetch)
- A synchronous function call that returns another generator so we can chain stuff
That is pretty much all the different types of function calls that I see in my daily JavaScript work. Let’s start with the simplest one - handle synchronous function call outside of the generator.
Handle synchronous function calls
First we need a function for creating the command object. We don’t want to write { action: <something> }
all the time so it will be nice if we have a helper for that.
function call(func, ...args) {
return { type: 'call', func, args };
}
call(mySynchronousFunction, 'foo', 'bar');
// { type: 'call', func: <mySynchronousFunction>, args: ['foo', 'bar' ] }
For the purpose of this article we may skip the type
key because all we are going to do is calling functions but it is a good idea to make that process explicit. Later we may decide to extend this layer and add something different like fetching data from a store or dispatching an action (if we work in Flux-ish context).
Let’s use the same player
concept and say that our main object has just two methods - moveLeft
and moveRight
. They will update an internal variable position
by given steps
. We also have a getPosition
which simply returns the value of the position
variable.
const player = function () {
var position = 0;
return {
moveLeft(steps) {
position -= steps;
},
moveRight(steps) {
position += steps;
},
getPosition() {
return position;
}
}
}
Now it gets interesting. We have to write a generator function that uses the call
helper to execute the methods of the player
.
function* game(player) {
yield call(player.moveLeft, 2);
yield call(player.moveRight, 1);
const position = yield call(player.getPosition);
console.log(`The position is ${ position }.`);
}
We basically say “Move the player two steps to the left and one step to the right. Then give me the player’s position”. The game
generator itself is doing nothing. That is because we yield
only JavaScript objects. Instructions of what we want to happen but without doing it. We could easily write the following equivalent:
function* game(player) {
yield { type: 'call', func: player.moveLeft, args: [2] };
yield { type: 'call', func: player.moveRight, args: [1] };
const position = yield { type: 'call', func: player.getPosition, args: [] };
console.log(`The position is ${ position }.`);
}
The next step in our implementation is to build the receiver. The bit which iterates the generator and executes our commands.
function receiver(generator) {
const iterate = function ({ value, done }) {
if (done) return value;
if (value.type === 'call') {
const result = value.func(...value.args);
return iterate(generator.next(result));
}
}
return iterate(generator.next());
}
receiver(game(player()));
/*
The result in the console is "The position is -1".
*/
The first thing that we do in the receive is to call generator.next
and pass the result to our internal iterate
function. It will be responsible for recursively calling next
till we complete the generator. It also makes sure that we resume the generator with the result of the last executed command. There are four calls of iterate
:
done
isfalse
andvalue
contains amoveLeft
commanddone
isfalse
andvalue
contains amoveRight
commanddone
isfalse
andvalue
contains agetPosition
commanddone
istrue
andvalue
isundefined
because we don’t have a return statement in ourgame
generator.
Handling a command that returns a promise
What if we want to save the position in a database via API. Let’s write a save
function in our player
which simulates an async process.
function player() {
var position = 0;
return {
moveLeft(steps) {...},
moveRight(steps) {...},
getPosition() {...},
save() {
return new Promise(resolve => setTimeout(() => resolve('successful'), 1000));
}
}
}
When we call save
we will receive a promise which gets resolved a second later. Inside the game
generator the usage of that function will look synchronous but in fact is not:
function* game(player) {
yield call(player.moveLeft, 2);
yield call(player.moveRight, 1);
const position = yield call(player.getPosition);
console.log(`The position is ${ position }.`);
const resultOfSaving = yield call(player.save);
console.log(`Saving is ${ resultOfSaving }.`);
}
Our receiver
now has to be smart enough to understand that the result of this particular command is a promise. It should also wait till that promise is resolved and resume the generator with the resolved value.
function receiver(generator) {
const iterate = function ({ value, done }) {
if (done) return value;
if (value.type === 'call') {
const result = value.func(...value.args);
if (result && typeof result.then !== 'undefined') { // <-- Oh wait, that's a promise
result.then(resolvedValue => iterate(generator.next(resolvedValue)));
} else {
return iterate(generator.next(result));
}
}
}
return iterate(generator.next());
}
We now examine the result of the command and check if it has a then
method. If yes we assume that this is a promise. We wait till it is resolved and again continue with the same recursion. If we run the code we will see The position is -1.
and then a second later Saving is successful.
. Here we can see the beauty of this pattern. Because of the pause-resume characteristic of the generator we are able to handle an asynchronous operation and hide it behind synchronous code.
Running a function that returns a generator
Let’s extract the two console logs into a separate generator called finish
:
function* finish(player) {
const position = yield call(player.getPosition);
console.log(`The position is ${ position }.`);
const resultOfSaving = yield call(player.save);
console.log(`Saving is ${ resultOfSaving }.`);
}
function* game(player) {
yield call(player.moveLeft, 2);
yield call(player.moveRight, 1);
yield call(finish, player);
console.log('finish');
}
The trivial approach for handing this case is to call the receiver
again with the result of the command. The code looks like this:
function receiver(generator) {
const iterate = function ({ value, done }) {
if (done) return value;
if (value.type === 'call') {
const result = value.func(...value.args);
if (result && typeof result.then !== 'undefined') {
result.then(resolvedValue => iterate(generator.next(resolvedValue)));
} else if (result && typeof result.next !== 'undefined') { // <-- Oh wait, that's a generator
return iterate(generator.next(receiver(result)));
} else {
return iterate(generator.next(result));
}
}
}
return iterate(generator.next());
}
So, if it happens that the result of the command is another generator we iterate over it again using the same receiver
function. The thing is that the new line iterate(generator.next(receiver(result)))
is actually synchronous while we may have asynchronous processes in that new generator. If we run the code above we will see:
The position is -1.
finish
Saving is successful.
While finish
should be displayed at the end. So, yield call(finish, player)
is not blocking the generator.
We have to be smarter and say “Ok, run the new generator but let me know when it is completed so I can continue iterating the main one.”. To satisfy this case we have to make our receiver
a little bit more complicated and assume that it always works asynchronously.
function receiver(generator) {
return new Promise(generatorCompleted => {
const iterate = function ({ value, done }) {
if (done) {
return generatorCompleted(value);
}
if (value.type === 'call') {
const result = value.func(...value.args);
if (result && typeof result.then !== 'undefined') {
result.then(resolvedValue => iterate(generator.next(resolvedValue)));
} else if (result && typeof result.next !== 'undefined') {
receiver(result).then(resultOfGenerator => {
iterate(generator.next(resultOfGenerator))
});
} else {
return iterate(generator.next(result));
}
}
}
iterate(generator.next());
});
}
Now the receiver
function returns a promise. It gets resolved when the generator is completed. If done
is true
we simply resolve the promise. Which perfectly cover our case and helps us asynchronously handle the internal generator.
receiver(result).then(resultOfGenerator => {
iterate(generator.next(resultOfGenerator))
});
Chaining generators
Instead of using call
for chaining with another generator we could simply yield
it like so:
function* game(player) {
yield call(player.moveLeft, 2);
yield call(player.moveRight, 1);
yield * finish(player);
console.log('finish');
}
Guess what? We don’t have to change our receiver
to make this work. It just works because when we use yield *
we are delegating a generator. For the code that iterates, the whole thing looks like a single generator. We just continue calling next
until we pass all the yield
statements (in the main AND delegated generators).
Handling errors
So far everything was working with no issues. But what if some of our commands throws an error. Let’s say that our player can not jump. If someone tries to make it jump we throw an error:
function player() {
var position = 0;
return {
moveLeft(steps) {...},
moveRight(steps) {...},
getPosition() {...},
save() {...},
jump() {
throw new Error(`You ain't jump!`);
}
}
}
To handle the error we have to wrap the execution of the command in a try-catch block:
function receiver(generator) {
return new Promise(generatorCompleted => {
const iterate = function ({ value, done }) {
if (done) { return generatorCompleted(value); }
if (value.type === 'call') {
try {
// calling value.func(...value.args)
// checking for a promise or another generator
// ...
} catch(error) {
iterate(generator.throw(error));
}
}
}
iterate(generator.next());
});
}
This is the first time where we see generator.throw
method. It resumes the generator by throwing an error inside. It is a really nice way to say “Hey, I got an error from your command. Here it is, handle it.”. Together with throwing an error throw
is a little bit like calling next
it moves the generator forward and we got again { done: ..., value: ... }
object as a result. So, we just pass it to the iterate
function in order to continue the recursion. Here is how we handle the error in the game
generator function:
function* game(player) {
yield call(player.moveLeft, 2);
yield call(player.moveRight, 1);
try {
yield call(player.jump);
} catch(error) {
console.log(`Ops, ${ error }`);
}
yield call(finish, player);
console.log('finish');
}
And the result in the console is:
Ops, Error: You ain't jump!
The position is -1.
Saving is successful.
finish
That is nice, we handled a synchronous command error. What if some of our async processes fail? Let’s create another method in our player that again returns a promise but that promise gets rejected:
function player() {
var position = 0;
return {
moveLeft(steps) { position -= steps; },
moveRight(steps) { position += steps; },
getPosition() { return position; },
save() {...},
jump() {...},
cheat() {
return new Promise((resolve, reject) => {
setTimeout(() => reject('sorry'), 1000)
});
}
}
}
The receiver
now has to be aware of the fact that the promise may be rejected and should again use throw
to send the error in our game
generator. The change that we have to do is around the code that handles the promise. then
method accepts a second argument which is function fired when the promise is rejected. We just do the same - continue the iteration by calling iterate
with generator.throw
's result as a parameter.
if (result && typeof result.then !== 'undefined') {
result.then(
resolvedValue => iterate(generator.next(resolvedValue)),
error => iterate(generator.throw(error))
);
}
In order to catch the error we have to again wrap our yield call
into a try-catch block.
function* game(player) {
yield call(player.moveLeft, 2);
yield call(player.moveRight, 1);
try {
yield call(player.jump);
} catch(error) {
console.log(`Ops, ${ error }`);
}
try {
yield call(player.cheat);
} catch (error) {
console.log(`Ops, ${ error }`);
}
yield call(finish, player);
console.log('finish');
}
Now the result of the whole thing becomes:
Ops, Error: You ain't jump!
Ops, sorry
The position is -1.
Saving is successful.
finish
This is how we handle errors. It first happens in the code that iterates (the receiver
) and then the errors are passed down to the generator.
Here is the final code of our receiver:
function receiver(generator) {
return new Promise(generatorCompleted => {
const iterate = function ({ value, done }) {
if (done) { return generatorCompleted(value); }
if (value.type === 'call') {
try {
const result = value.func(...value.args);
if (result && typeof result.then !== 'undefined') {
result.then(
resolvedValue => iterate(generator.next(resolvedValue)),
error => iterate(generator.throw(error))
);
} else if (result && typeof result.next !== 'undefined') {
receiver(result).then(resultOfGenerator => {
iterate(generator.next(resultOfGenerator))
});
} else {
return iterate(generator.next(result));
}
} catch(error) {
iterate(generator.throw(error));
}
}
}
iterate(generator.next());
});
}
And here is a CodePen to play with it:
See the Pen Implementation of the command pattern using generators by Krasimir Tsonev (@krasimir) on CodePen.
Using a library
I learned this pattern from the redux-saga project. You will see a similar call
helper there but the library is Redux specific. So I decided to extract the code above into a npm module. Here is the same example but using banica library.
import { run, call } from 'banica';
function player() {
var position = 0;
return {
moveLeft(steps) { ... },
moveRight(steps) { ... },
getPosition() { ... },
save() { ... },
jump() { ... },
cheat() { ... }
}
}
function* finish(player) {
const position = yield call(player.getPosition);
console.log(`The position is ${ position }.`);
const resultOfSaving = yield call(player.save);
console.log(`Saving is ${ resultOfSaving }.`);
}
function* game(player) {
yield call(player.moveLeft, 2);
yield call(player.moveRight, 1);
try {
yield call(player.jump);
} catch(error) { console.log(`Ops, ${ error }`); }
try {
yield call(player.cheat);
} catch (error) { console.log(`Ops, ${ error }`); }
yield call(finish, player);
console.log('finish');
}
run(game(player()));
(Why I call it “banica”? Well, that’s one of my favorite Bulgarian dishes. More about it here).
Final words
This type of command pattern implementation together with the idea of the state machines are game changers for me this year. I hope you enjoy this article and I made you experiment more with generators. And why not try redux-saga or banica libraries.