Skip to content

Instantly share code, notes, and snippets.

@ChrisChares
Last active September 30, 2022 13:26
Show Gist options
  • Save ChrisChares/1ed079b9a6c9877ba4b43424139b166d to your computer and use it in GitHub Desktop.
Save ChrisChares/1ed079b9a6c9877ba4b43424139b166d to your computer and use it in GitHub Desktop.
async/await with ES6 Generators & Promises

async/await with ES6 Generators & Promises

This vanilla ES6 function async allows code to yield (i.e. await) the asynchronous result of any Promise within. The usage is almost identical to ES7's async/await keywords.

async/await control flow is promising because it allows the programmer to reason linearly about complex asynchronous code. It also has the benefit of unifying traditionally disparate synchronous and asynchronous error handling code into one try/catch block.

This is expository code for the purpose of learning ES6. It is not 100% robust. If you want to use this style of code in the real world you might want to explore a well-tested library like co, task.js or use async/await with Babel. Also take a look at the official async/await draft section on desugaring.

Compatibility

  • node.js - 4.3.2+ (maybe earlier with --harmony flag)
  • browser - (if you have Promises && Generators)
  • babel 👍 { "presets": ["es2015"] }

Background Reading

Furthur

  • Return Promise from async to support chaining
  • Support other asynchronous actions like thunks or nested generators
/**
 *	@param {Generator} gen Generator which yields Promises
 *	@param {Any} context last resolved Promise value, currently only used recursively
 */
function async(gen, context = undefined) {
	const generator = typeof gen === 'function' ? gen() : gen; // Create generator if necessary
	const { value: promise } = generator.next(context); // Pass last result, get next Promise
	if ( typeof promise !== 'undefined' ) {
		promise.then(resolved => async(generator, resolved))
		.catch(error => generator.throw(error)); // Defer to generator error handling
	}
}

/* Usage */
async(function* () { // Generators can't be declared with arrow syntax
	try {
		// Execution is paused until the yielded promise resolves
		console.log(yield Promise.resolve('A Mortynight Run'))
		// Promises already provide support for concurrent async actions.
		// Execution will not continue until both a & b are fulfilled
		const [a,b] = yield Promise.all([
			Promise.resolve('Get Schwifty'),
			Promise.resolve('The Ricks Must Be Crazy')
		]);
		console.log(a + ' ' + b);
		// Rejected promises will be handled by try/catch as if code was synchronous
		const seasonTwoFinale = yield Promise.reject(new Error('Tammy'));
		// Never executed
		const seasonThree = 'Coming Soon';
	} catch (error) {  console.error(error.message);  } // Tammy
})
@getify
Copy link

getify commented Feb 2, 2017

May I suggest renaming some variables for clarity:

function async(it, context = undefined) {
    let iterator = typeof it === 'function' ? it() : it // Create iterator if necessary
    let { value: promise } = iterator.next(context) // Pass last result, get next Promise
    if ( typeof promise !== 'undefined' ) {
        promise.then(resolved => async(iterator, resolved))
        .catch(error => iterator.throw(error)) // Defer to generator error handling
    }
}

Some other things to point out that seem missing here:

  1. an async(..) call should return a promise, as all calls to async functions do. This promise should resolve to the final return value of the generator.

  2. If I yield 42 (aka, await 42), that's supposed to get wrapped in an immediately resolved promise and thus fed right back into the generator. Here, you assume it's always a promise, and call .then(..) on it. You should probably use Promise.resolve(..) on the return value, for safety sake.

  3. If a yielded promise rejects, you correctly throw(..) it back into the generator... but if the generator has a try..catch around that yield and catches the throw()n error, and then subsequently yields/returns another promise or value, your runner is not capturing that value in resolved and continuing the loop by passing it back to the next next(..) call. In other words, your runner will stop prematurely. It breaks on code like this:

    async(function* foo() {
       try {
          yield Promise.reject(42);
       }
       catch(err) {
          // this code runs
          console.log("caught:",err);
       }
       // this code runs
       var x = yield Promise.resolve("hello world");
    
       // but, this code never runs because your runner stopped prematurely
       console.log(x);
    })
  4. Your detection of the generator being finished is flawed, assuming value: undefined means it's complete. That's not a reliable assumption. I could return 42 from my generator, in which case that value would come out with value: 42, done: true and wouldn't detect that it was complete, so you'd try to resume an already complete generator. Or, I could yield undefined, which would be value: undefined, done: false, meaning the generator is most definitely not complete, but your runner would end prematurely.

  5. Your async(..) helper isn't supporting passing any arguments into the original generator, the way you can when actually calling an async function.


To address these issues (and others), I have a commented full-implementation of a promise-aware generator runner (emulating the sync-async style as your's is) here: https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch4.md#promise-aware-generator-runner

@ChrisChares
Copy link
Author

@getify hey thanks for commenting! It was your blog post on ES6 generators that set me down this path and got me thinking about different kinds of generator applications in the first place! Really helpful stuff. Every other post seemed to be focused on simplistic linear number generators, you had the first article I found that explained the control-flow of a non-trivial generator.

I got a little too caught up on having as short and concise an implementation as possible with this so a lot of stuff got left out. Thanks for all the suggestions and improvements! I'm headed out for vacation today but I'm looking forward to working through your improvements when I get back.

@xgrommx
Copy link

xgrommx commented Feb 8, 2017

@ChrisChares My version works with infinity of nested generators. Also superposition is Observable instead of Promise. Reactive-Extensions/RxJS#855 (comment) The same for mostjs https://github.com/xgrommx/most-spawn also some experiments with coroutines https://github.com/xgrommx/async-fun

@rgeraldporter
Copy link

} catch (error) { console.error(error.message); // Tammy }
You've commented out the closing }.

@ChrisChares
Copy link
Author

@rgeraldporter fixed, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment