Angular 2 Tutorial with Async and Await

January 27th 2017 Async TypeScript Angular 2

In version 2.1 that was released in December 2016, TypeScript finally added support for downlevel transpilation of async and await keywords for ES3 and ES5 targets. This means that you can write asynchronous functions to work with promises, no matter which JavaScript environment you are targeting. To give this new feature a try, I decided to convert the Angular 2 Tour of Heroes tutorial to use async and await.

Of course, we must first update TypeScript in the project to a recent enough version (i.e. 2.1 or newer) to get the required support:

npm update typescript --save-dev

If we inspect the final the code at the end of the tutorial, we can see that promises are only used in HeroService and any components that use it. We best start by updating the service, function by function.

This is getHeroes() before the change:

getHeroes(): Promise<Hero[]> {
  return this.http.get(this.heroesUrl)
    .toPromise()
    .then(response => response.json().data as Hero[])
    .catch(this.handleError);
}

To make the function asynchronous, we need to do three changes:

  • Add async keyword to the function declaration.
  • Instead of calling then() on the promise, await it and move the callback code to main function body. The promise result required in the callback will be returned by the await call.
  • Replace the catch call with a try-catch block.

Here's the new code:

async getHeroes(): Promise<Hero[]> {
  try {
    let response = await this.http
      .get(this.heroesUrl)
      .toPromise();
    return response.json().data as Hero[];
  } catch (error) {
    await this.handleError(error);
  }
}

Two more things to notice:

  • We are returning a Hero[] instance directly. The asynchronous function takes care of wrapping it into a Promise.
  • We are also using await to call handleError() because it returns a rejected promise. This will result in an error being thrown from getHeroes().

The next method will introduce us to a new concept:

getHeroesSlowly(): Promise<Hero[]> {
  return new Promise<Hero[]>(resolve =>
    setTimeout(resolve, 2000))
    .then(() => this.getHeroes());
}

We are creating a new promise ourselves here. We could await it directly:

async getHeroesSlowly(): Promise<Hero[]> {
  await new Promise<Hero[]>(resolve =>
    setTimeout(resolve, 2000));
  return await this.getHeroes();
}

Instead we will wrap the promise creation into a new method to make it reusable elsewhere:

private delay(ms: number)>: Promise<void> {
  return new Promise<void>(resolve =>
    setTimeout(resolve, ms));
}

Since we are only returning a Promise from the method and not waiting for any promises to resolve, the method can be synchronous. Now, we can call it from getHeroesSlowly() and make it more readable:

async getHeroesSlowly(): Promise<Hero[]> {
  await this.delay(2000);
  return await this.getHeroes();
}

The remaining service methods are quite similar, therefore I'll just list them here already converted for the sake of completeness:

async getHero(id: number): Promise<Hero> {
  let heroes = await this.getHeroes();
  return heroes.find(hero => hero.id === id);
}

async update(hero: Hero): Promise<Hero> {
  try {
    const url = `${this.heroesUrl}/${hero.id}`;
    await this.http
      .put(url, JSON.stringify(hero), {headers: this.headers})
      .toPromise();
    return hero;
  } catch (error) {
    await this.handleError(error);
  }
}

async create(name: string): Promise<Hero> {
  try {
    let res = await this.http
      .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
      .toPromise();
    return res.json().data;
  } catch (error) {
    await this.handleError(error);
  }
}

async delete(id: number): Promise<void> {
  try {
    const url = `${this.heroesUrl}/${id}`;
    await this.http.delete(url, {headers: this.headers})
      .toPromise();
  } catch (error) {
    await this.handleError(error);
  }
}

Since the asynchronous functions return promises just like their synchronous counterparts did, we could stop here without making any changes to service consumers, allowing us to gradually transition from synchronous to asynchronous approach.

We won't stop here, of course. Rather, we'll continue with DashboardComponent.ngOnInit(). This is its code before the change:

ngOnInit(): void {
  this.heroService.getHeroes()
   .then(heroes => this.heroes = heroes.slice(1, 5));
}

An asynchronous function must return a promise, even if didn't before the change. In our case, we'll change the return value to Promise<void>:

async ngOnInit(): Promise<void> {
  let heroes = await this.heroService.getHeroes();
  this.heroes = heroes.slice(1, 5);
}

We'll make similar changes to functions in HeroesComponent:

async getHeroes(): Promise<void> {
  let heroes = await this.heroService.getHeroesSlowly();
  this.heroes = heroes;
}

async add(name: string): Promise<void> {
  name = name.trim();
  if (!name) { return; }
  let hero = await this.heroService.create(name);
  this.heroes.push(hero);
  this.selectedHero = null;
}

async delete(hero: Hero): Promise<void> {
  await this.heroService.delete(hero.id);
  this.heroes = this.heroes.filter(h => h !== hero);
  if (this.selectedHero === hero) { this.selectedHero = null; }
}

And HeroDetailComponent as well:

async save(): Promise<void> {
  await this.heroService.update(this.hero);
  this.goBack();
}

With this final change, we are done. There isn't a single call to Promise.then() left in our project. We converted every single one of them to an asynchronous call. If you're curious, you can check the transpiled JavaScript code to see what TypeScript compiler does for you in the background. To learn more about it, check a blog post on that very topic.

Copyright
Creative Commons License