We're planting a tree for every job application! Click here to learn more

Implementing Async And Await With Generators

Maciej Cieślar

12 Oct 2018

4 min read

Implementing Async And Await With Generators
  • async

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!

Did you like this article?

Maciej Cieślar

Full Stack Web Developer

See other articles by Maciej

Related jobs

See all

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Related articles

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

12 Sep 2021

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

12 Sep 2021

WorksHub

CareersCompaniesSitemapFunctional WorksBlockchain WorksJavaScript WorksAI WorksGolang WorksJava WorksPython WorksRemote Works
hello@works-hub.com

Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ

108 E 16th Street, New York, NY 10003

Subscribe to our newsletter

Join over 111,000 others and get access to exclusive content, job opportunities and more!

© 2024 WorksHub

Privacy PolicyDeveloped by WorksHub