author: Krasimir Tsonev

Krasimir is a blogger, who writes and speaks.
He loves open source and codes awesome stuff.

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 yielded. 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 is false and value contains a moveLeft command
  • done is false and value contains a moveRight command
  • done is false and value contains a getPosition command
  • done is true and value is undefined because we don’t have a return statement in our game 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:

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.

blog comments powered by Disqus