Ember and ES7: async / await
In the previous blog post we were exploring a new wonderful feature coming with ECMAScript 7: decorators. This time we are going to learn about async / await
, which is at this moment at Stage 3 in TC39 process, which means it's already a release candidate that passed Proposal and Draft stages. Just like decorators, it's already available in Babel. Let's see what kind of benefits does it offer.
A little history of writing asynchronous code in JavaScript
The most basic and only available solution to write asynchronous code until recently was using callbacks. Let's check example with multiple HTTP requests with fetching some user with id 1 and creating some task list and task for this user assuming that we have HTTPService
that performs GET
and POST
requests:
HTTPService.get('/users/1', functions(user) {
HTTPService.post('/task_lists', { name: `default for ${user.name}`, user_id: user.id }, function(taskList) {
HTTPService.post('/tasks', { name: 'finish setup', task_list_id: taskList.id }, function(task) {
console.log(`created task ${task.id} for user ${user.id}`);
}, function(error) {
console.log(error);
});
}, function(error) {
console.log(error);
});
});
}, function(error) {
console.log(error);
});
Well, it's not exactly readable. Every inner function depends on the result from previous function which easily leads to Callback Hell a.k.a. Pyramid of Doom. Adding error handling for every callback makes it even worse. Could we somehow make it linear and more readable?
The answer is yes, thanks to promises. I assume that if you use Ember you already know what they are and how they work as they are pretty common in this framework, but for better undertanding you may want to check the reference. Let's imagine that our HTTPService
returns a promise instead of expecting callback-flow. How would the code look in such case?
HTTPService.get('/users/1').then(user => {
return HTTPService.post('/task_lists', { name: 'default', user_id: user.id });
}).then(taskList => {
return HTTPService.post('/tasks', { name: 'finish setup', task_list_id: taskList.id });
}).then(task => {
console.log(`created task ${task.id} for user ${user.id}`);
}).catch(error => {
console.log(error);
});
Isn't it much cleaner? We made the consecutive function calling linear and simplified error handling by having generic function chained in the end.
But still, something doesn't seem right. Promises make the code much better comparing to callbacks, but the syntax itself is a bit heavy as it's totally different than the standard synchronous code. Is it even possible to write asynchronous code (almost) the same way as a synchronous one? Sure it is! Say hello to your new friend: async / await
.
async / await keywords - what they are how to use them
Imagine for a moment that HTTPService
performs synchronous operations. How different would be a code flow when using it? Let's give it a try:
let user = HTTPService.get('/users/1');
let taskList = HTTPService.post('/task_lists', { name: 'default', user_id: user.id });
let task = HTTPService.post('/tasks', { name: 'finish setup', task_list_id: taskList.id });
console.log(`created task ${task.id} for user ${user.id}`);
What about error handling? We can use try / catch
construct to catch any error:
try {
let user = HTTPService.get('/users/1');
let taskList = HTTPService.post('/task_lists', { name: 'default', user_id: user.id });
let task = HTTPService.post('/tasks', { name: 'finish setup', task_list_id: taskList.id });
console.log(`created task ${task.id} for user ${user.id}`);
} catch (error) {
console.log(error);
}
The amazing thing is that we can preserve that flow, even though HTTPService returns promises! We just need to await
for the result of the asynchronous function call:
try {
let user = await HTTPService.get('/users/1');
let taskList = await HTTPService.post('/task_lists', { name: 'default', user_id: user.id });
let task = await HTTPService.post('/tasks', { name: 'finish setup', task_list_id: taskList.id });
console.log(`created task ${task.id} for user ${user.id}`);
} catch (error) {
console.log(error);
}
Compare this code to the first version with Pyramid of Doom or even the then
chaining in promises - it looks perfectly natural now. Basically, await
is a keyword indicating that we wait for the promise to be fulfilled in this place. What about async
? Every function that uses await
keyword is async
so to make our code actually usable we need to wrap it in such function by using this keyword before its definition:
async function setUpDefaultTask() {
try {
let user = await HTTPService.get('/users/1');
let taskList = await HTTPService.post('/task_lists', { name: 'default', user_id: user.id });
let task = await HTTPService.post('/tasks', { name: 'finish setup', task_list_id: taskList.id });
console.log(`created task ${task.id} for user ${user.id}`);
} catch (error) {
console.log(error);
}
}
From that moment you probably don't think about coming back to the old way of writing asynchronous code :).
async / await and Ember
To start using async / await
in your Ember app you just need to include polyfill in ember-cli-build.js
:
var EmberApp = require('ember-cli/lib/broccoli/ember-app');
module.exports = function(defaults) {
var app = new EmberApp({
babel: {
includePolyfill: true
}
});
return app.toTree();
};
Otherwise you will get error with regeneratorRuntime
being undefined. You may also consider disabling jshint
which sadly doesn't support async / await
yet. You might also think about switching to eslint with babel-esling
, which supports every feature implemented in Babel.
Where could it be used? The good canditate would be actions
functions in your components / controllers where you probably have a lot of promises with some API calls. Imagine that you have some action with creating a task and transitioning to the task
route. With async / await
you could simply write it like that:
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
async save() {
let task = await this.get('task').save();
this.transitionToRoute('tasks.show', task);
}
}
});
Which is much simpler than using then
:
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
save() {
this.get('task').save().then(() => {
this.transitionToRoute('tasks.show', task);
}
}
}
});
The other usecase would be acceptance tests. I always forget about using andThen
when writing new tests and get failures in something that looks as a valid test. If that's also a case with you then switching to async / await
will solve that problem forever. Here's a little example with using traditional approach with andThen
:
import Ember from 'ember';
import {
module,
test
} from 'qunit';
var application;
module('Acceptance: SomeTest', {
beforeEach: function() {
application = startApp();
},
afterEach: function() {
Ember.run(application, 'destroy');
}
});
test('it clicks button', function(assert) {
click('.some-button');
andThen(function() {
assert.equal(find('.clicked-button').length, 1);
});
});
Not really ideal. The example below looks much better:
import Ember from 'ember';
import {
module,
test
} from 'qunit';
var application;
module('Acceptance: SomeTest', {
beforeEach: function() {
application = startApp();
},
afterEach: function() {
Ember.run(application, 'destroy');
}
});
test('it clicks button', async function(assert) {
await click('.some-button');
assert.equal(find('.clicked-button').length, 1);
});
Much more intuitive, we just need to remember about adding async
before callback in test
function, but we are going to get syntax error if we use await
in not async
function, so the issue will be obvious.
Wrapping up
async / await
is another excellent addition in Javascript world, which may become a real game changer when it comes to writing asynchronous code. Thanks to Babel again, we can easily start using it even today.