Nowadays we can write our asynchronous code in a synchronous way thanks to the async and await keywords, which makes it easier to read and understand. Recently I wondered, however, how could the same effect be achieved without using these keywords?

It turns out to be quite simple since the behavior of async and await can easily be emulated using generators. Let’s have a look!

Go ahead, clone the repository and let’s get started.

Generators

I am going to assume you have little to no experience with generators since, honestly, most of the time they aren’t particularly useful and you can easily manage without them. Don’t worry, however, we’ll start with a quick reminder:

Generators are objects created by generator functions - functions with * (asterisk) next to their name.

These generators have the amazing ability that lets us stop the execution of code - whenever we want - by using the keyword yield.

Consider this example:

const generator = (function*() {
  // waiting for .next()
  const a = yield 5
  // waiting for .next()
  console.log(a) // => 15
})()

console.log(generator.next()) // => { done: false, value: 5 }
console.log(generator.next(15)) // => { done: true, value: undefined }

Given that these are absolute basics I would recommend that, before you scroll any further, you read this article to get a grasp on what is really going on here.

If you feel like you have a strong understanding of the underlying ideas - we can move on.

Hold on, await a minute

Haven’t you ever wondered how await really works?

Somehow it just waits for our promise to return a value and proceed with the execution. For me, that seems like something generator would be able to do after a little tweaking.

What we could do, is just take every yielded value, put it into a promise, then wait for the promise to be resolved and afterwards return it to the generator by calling generator.next(resolvedValue).

Sounds like a plan, but first, let’s write some tests just to be sure that everything is working as expected.

What should our asynq function do:

  • wait for asynchronous code before continuing the execution
  • return a promise with the returned value from the function
  • make try/catch work on asynchronous code

Note: because we are using generators, our await becomes yield.

Alright, great! Now we can talk about the implementation.

Our asynq function takes as a parameter a function generator - by calling it, we create a generator.

Just to be sure, we call isGeneratorLike which checks if the received value is an object and has methods next and throw.

Then, recursively, we consume each yield keyword by calling generator.next(ensuredValue), waiting for the returned promise to be settled and then return its result back to the generator by repeating the whole process.

We must also attach the catch handler, so that, should the function throw an exception, we can catch it and return the exception inside the function by calling generator.throw(error).

Now, any potential errors will be handled by catch. If there wasn’t a try/catch block in place, an errorwould simply stop the execution altogether - like any unhandled exception would - and our function would return a rejected promise.

When the generator is done, we return the generator’s return value in a promise.

Now, having run our tests we can see that everything is working as expected.


While this implementation is probably not the one used inside the JavaScript engines, it sure feels good to be able to do something like this on our own.

Feel free to go over the code again, the better understanding of the underlying ideas you have here, the more you will be able to appreciate the brilliance of the creators of async and await keywords.


Thank you very much for reading! I hope you found this article informative and that it helped you to see there is no magic involved in async and await keywords and that they could be easily replaced with generators.

Also available on Medium.