We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel ๐Ÿš€.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Engineers who find a new job through JavaScript Works average a 15% increase in salary ๐Ÿš€

Blog hero image

Implementing Async And Await With Generators

Maciej Cieล›lar 9 October, 2018 | 4 min read

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.

import { asynq } from '../src';

describe('asynq core', () => {
  test('Waits for values (like await does)', () => {
    return asynq(function*() {
      const a = yield Promise.resolve('a');
      expect(a).toBe('a');
    });
  });

  test('Catches the errors', () => {
    return asynq(function*() {
      const err = new Error('Hello there');

      try {
        const a = yield Promise.resolve('a');
        expect(a).toBe('a');

        const b = yield Promise.resolve('b');
        expect(b).toBe('b');

        const c = yield Promise.reject(err);
      } catch (error) {
        expect(error).toBe(err);
      }

      const a = yield Promise.resolve(123);
      expect(a).toBe(123);
    });
  });

  test('Ends the function if the error is not captured', () => {
    const err = new Error('General Kenobi!');

    return asynq(function*() {
      const a = yield Promise.reject(err);
      const b = yield Promise.resolve('b');
    }).catch((error) => {
      expect(error).toBe(err);
    });
  });

  test('Returns a promise with the returned value', () => {
    return asynq(function*() {
      const value = yield Promise.resolve(5);
      expect(value).toBe(5);

      return value;
    }).then((value) => {
      expect(value).toBe(5);
    });
  });
});

The tests of asynq


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 error would 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.

import { isGeneratorLike } from './utils';

type GeneratorFactory = () => IterableIterator<any>;

function asynq(generatorFactory: GeneratorFactory): Promise<any> {
  const generator = generatorFactory();

  if (!isGeneratorLike(generator)) {
    return Promise.reject(
      new Error('Provided function must return a generator.'),
    );
  }

  return (function resolve(result) {
    if (result.done) {
      return Promise.resolve(result.value);
    }

    return Promise.resolve(result.value)
      .then((ensuredValue) => resolve(generator.next(ensuredValue)))
      .catch((error) => resolve(generator.throw(error)));
  })(generator.next());
}

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.

If you have any questions or comments feel free to put them in the comment section below or send me a message

Check out my social media!

Join my newsletter!

Originally published on www.maciejcieslar.com

Author's avatar
Maciej Cieล›lar
Full Stack Web Developer
    JavaScript
    TypeScript
    HTML
    CSS
    React
    Node

Related Issues

viebel / klipse-clj
viebel / klipse-clj
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
  • $100
viebel / klipse
  • 1
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
  • $80
viebel / klipse
  • Open
  • 0
  • 0
  • Advanced
  • Clojure
  • $80
viebel / klipse
  • Started
  • 0
  • 2
  • Advanced
  • Clojure
  • $180
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 3
  • Intermediate
  • Clojure
  • $80

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through JavaScript Works average a 15% increase in salary.

Start with GithubStart with Stack OverflowStart with Email