http://channel9.msdn.com/Blogs/Seth-Juarez/Async-Functions-in-JavaScript-with-Etienne-Baudoux
ES7/ES2016 Async Functions
The async
and await
keywords, part of the Async Functions proposal, provide an easy way to write asynchronous code. It is one of the killer features of C# and is often requested by JavaScript developers. Before Async Functions and Promises, a JavaScript developer had to carve out all the asynchronous code she needed into a separate function from the synchronous core and use callbacks to retrieve the result of the asynchronous computation. This code can quickly become hard to read and difficult to maintain.
ES6 Promises helped improve the way to write asynchronous code by standardizing and by including the feature directly in the web browser. But it did not totally solve the problem as we still need to use callbacks to retrieve the result.
Async Functions are based on Promises, but take them a step further. When you add an async
keyword to a function or an arrow function it will automatically return a Promise. For example, the following code is a standard ES2015 program that performs an Http request with promises:
// ES6 code, without async/await | |
function httpGet(url) { | |
return new Promise(function (resolve, reject) { | |
// do the usual Http request | |
var request = new XMLHttpRequest(); | |
request.open('GET', url); | |
request.onload = function () { | |
if (request.status == 200) { | |
resolve(request.response); | |
} else { | |
reject(Error(request.statusText)); | |
} | |
}; | |
request.onerror = function () { | |
reject(Error('Network Error')); | |
}; | |
request.send(); | |
}); | |
} | |
function httpGetJson(url) { | |
return new Promise(function (resolve, reject) { | |
// check if the URL looks like a JSON file and call httpGet. | |
var regex = /\.(json)$/i; | |
if (regex.test(url)) { | |
// call the promise, wait for the result | |
resolve(httpGet(url).then(function (response) { | |
return response; | |
}, function (error) { | |
reject(error); | |
})); | |
} else { | |
reject(Error('Bad File Format')); | |
} | |
}); | |
} | |
httpGetJson('file.json').then(function (response) { | |
console.log(response); | |
}).catch(function (error) { | |
console.log(error); | |
}); |
If we rewrite the same code and get the same behavior with Async Functions, the result is smaller and more readable code. The code below also has a refactoring to change the way we manage the error in the function:
// ES7 code, with async/await | |
function httpGet(url) { | |
return new Promise(function (resolve, reject) { | |
// do the usual Http request | |
let request = new XMLHttpRequest(); | |
request.open('GET', url); | |
request.onload = function () { | |
if (request.status == 200) { | |
resolve(request.response); | |
} else { | |
reject(Error(request.statusText)); | |
} | |
}; | |
request.onerror = function () { | |
reject(Error('Network Error')); | |
}; | |
request.send(); | |
}); | |
} | |
async function httpGetJson(url) { | |
// check if the URL looks like a JSON file and call httpGet. | |
let regex = /\.(json)$/i; | |
if (regex.test(url)) { | |
// call the async function, wait for the result | |
return await httpGet(url); | |
} else { | |
throw Error('Bad Url Format'); | |
} | |
} | |
httpGetJson('file.json').then(function (response) { | |
console.log(response); | |
}).catch(function (error) { | |
console.log(error); | |
}); |
The async
keyword also works with ES6 arrow functions, simply by adding the keyword before the arguments. Following in an example of the same:
// call the async function, wait for the result | |
let func = async () => await httpGet(url); | |
return await func(); |
To clarify:
- Use the
async
keyword with any function declaration or arrow function to get asynchronous code with a Promise. This includes functions in classes and static function. For this last one, theasync
keyword should be placed after thestatic
keyword and before the function’s name. - Use the
await
keyword to make execution wait for the end of anasync
expression (a call to anasync
function for example) and get the returned value of the Promise. - If you don’t use the
await
keyword, you will get a Promise.
https://gist.github.com/kypflug/55fa7c7fa8f3bff1866c - You cannot use the
await
keyword outside anasync
function, including the global scope.
How is it implemented in Chakra?
In a previous post, we discussed the architecture of the Chakra engine in detail using the diagram below. The parts requiring the most modification to support Async Functions are highlighted in green.
Global behavior
Using the async
keyword will generate a Promise constructor, according to the specifications, that wraps the function’s body. To perform this action, Chakra’s Bytecode Generator generates a call to a built-in function that describe the following behavior:
function spawn(genF, self) { | |
return new Promise(function (resolve, reject) { | |
var gen = genF.call(self); | |
function step(nextF) { | |
var next; | |
try { | |
next = nextF(); | |
} catch (e) { | |
// finished with failure, reject the promise | |
reject(e); | |
return; | |
} | |
if (next.done) { | |
// finished with success, resolve the promise | |
resolve(next.value); | |
return; | |
} | |
// not finished, chain off the yielded promise and `step` again | |
Promise.resolve(next.value).then(function (v) { | |
step(function () { return gen.next(v); }); | |
}, function (e) { | |
step(function () { return gen.throw(e); }); | |
}); | |
} | |
step(function () { return gen.next(undefined); }); | |
}); | |
} |
The spawn function above is designed to manage all the async
expressions in the function’s body and decide to stop or continue the process depending of the behavior inside the async
function. If an async
expression called with an await
keyword fails, for example because of an error inside the async
function or something being awaited, the promise will be rejected to make it possible to manage the error higher in the stack.
Now the engine needs to make the JS script call this spawn function to get the Promise and run the function’s body. To do this, when the parser detects an async
keyword, the engine changes the AST (Abstract Syntax Tree), which represents the developer’s algorithm, to add a call to the Spawn function with the function’s body. Consequently, the httpGetJson
function is converted like this by the parser:
function httpGetJson(url) { | |
return spawn(function* () { | |
// check if the URL looks like a JSON file and call httpGet. | |
var regex = /\.(json)$/i; | |
if (regex.test(url)) { | |
// call the async function, wait for the result | |
return yield httpGet(url); | |
} else { | |
throw Error('Bad Url Format'); | |
} | |
}, this); | |
} |
Note the use of Generators and the yield
keyword to perform the await
keyword’s behavior. Indeed, the implementation of the await
keyword is extremely similar to the yield
keyword.
Behavior with a default argument
One of the new feature from ES6 is the possibility to set a default value to an argument in a function. When default value is set, the Bytecode Generator will set the value at the top of the function’s body.
// original JavaScript code | |
function foo(argument = true) { | |
// some stuff | |
} | |
// representation of the Bytecode Generator's output in JavaScript | |
function foo(argument) { | |
argument = true; | |
// some stuff | |
} |
In the case of an async
keyword, if the default value sets to this argument throw an error, the specifications ask to reject a Promise. This allows the exception to be caught easily.
To realize it, the Chakra team had two choices: change the AST (Abstract Syntax Tree), or implement it directly in the Bytecode Generator. We chose to implement it directly in the Bytecode Generator by moving the argument’s initialization to the top of the function’s body directly in the Bytecode, because it is an easier and cleaner solution in this engine. Since a try/catch
needs to be added to intercept any error from the argument’s default value, it’s easier to change the Bytecode directly when we detect an async
keyword.
Finally, the generated Bytecode will be similar to what would be generated for the following JavaScript code:
// representation of the Bytecode Generator's output in JavaScript | |
function foo(argument) { | |
try { | |
argument = true; | |
} catch (error) { | |
return Promise.reject(error); | |
} | |
return spawn(function* () { // keep this call as we are in an async function | |
// some stuff | |
}, this); | |
} |
How to enable Async Functions in Microsoft Edge?
To enable and experiment with the current Async Functions support in Microsoft Edge, navigate to about:flags in Microsoft Edge and select the “Enable experimental JavaScript features” flag as below:
Async Functions are available to preview via the Windows Insider Program starting in Microsoft Edge 13.10547.We can’t wait to hear about how you will be using the feature in your ES2016 code and look forward to sharing more soon about our next investments in Chakra. In the meantime, feel free to join the comment discussion, reach out on Twitter @MSEdgeDev, or on Connect.
– Etienne Baudoux, Software Development Engineer Intern, Chakra Team
– Brian Terlson, Senior Program Manager, Chakra Team