Karol Galanciak - Ruby on Rails and Ember.js consultant

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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?

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
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:

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.

Comments