Async/Await

Je suis enfin tombé sur un cas pratique où l'utilisation de nouveaux paradigme s'est imposé. J'ai cru bon d'abord d'aller fouiller vers les generators, j'ai bien cru que j'allais enfin pouvoir comprendre à quoi ils servaient. Mais finalement non. À la place, j'ai expérimenté un autre outil que je voulais essayer depuis un moment et qui a résolu mon problème : async/await.

Le problème est le suivant. Je veux pouvoir vérifier qu'un email est déjà utilisé par un utilisateur et retourner un boolean. J'ai une base redis qui contient des set users:<id> avec une entrée email. J'ai la flemme de faire un index à ce moment du prototypage, donc je veux récupérer toutes les clés users:* et pour chacune chercher l'entrée email, si il correspond à l'email demandé, je sors de la boucle pour ne pas exécuter les requêtes suivantes et je renvoie l'id correspondant.

Impossible de trouver autre chose dans bluebird permettant d'exécuter un lot de promesses en même temps. Alors j'ai jeté un œil aux generators, ça ne m'a pas aidé, mais en fouillant un peu, j'ai découvert que async/await était parfait pour répondre à mon problème.

Async permet de définir une fonction comme asynchrone et lui fait retourner une promesse tout en écrivant du code en apparence procédurale. Await permet, dans une fonction asynchrone, de mettre en pause l'exécution du script et d'attendre le résultat de l'expression qui lui est liée. S'il s'agit d'une promesse (dans le cas contraire, ça ne sert à rien), la résolution de la promesse sera retournée et le script reprendra son cours et le rejet provoquera une exception. On peut ainsi faire une simple boucle for pour lancer une requête après l'autre.

Résultat :

async getUserIdByEmail (email) {  
// cette fonction retournera automatiquement
// une promesse grâce à async
  let keys = await this.client.keysAsync('users:*');

  for (let key of keys) {
    let result = await this.client.hgetAsync(key, 'email');
    // await met le script en pause.
    // Dès que client.hgetAsync résoudra sa promesse,
    // le résultat sera placé dans la variable result et
    // le script reprendra
    if (result === email) {
      return key.match(/^users\:(\w+)$/)[1];
    }
  }

  return null;
}

Grâce à async, ma fonction retourne désormais une promesse sans que j'ai besoin de créer une new Promise(). Grâce à await, même si on se trouve dans une boucle, mon bloc est mis en attente jusqu'à ce que la promesse soit résolue ou rejetée. Je peux ainsi traiter chaque requêtes les unes après les autres comme si elles étaient synchrones. On peut donc stopper la boucle très facilement avec un return ou un break. La preuve avec quelques console.log :

async getUserIdByEmail (email) {  
  let keys = await this.client.keysAsync('users:*');

  for (let key of keys) {
    console.log(key);
    let result = await this.client.hgetAsync(key, 'email');
    console.log(result);
    if (result === email) {
      return key.match(/^users\:(\w+)$/)[1];
    }
  }

  return null;
}

getUserByEmail('hadrien@lanneau.me')  
.then(result => console.log(result))); // => 1

Sachant que dans ma base, seuls users:1 et users:2 existent :

La boucle s'est bien arrêtée dès que l'email a été trouvé et on évite de faire toutes les autres requêtes inutiles.

Pour l'utiliser, il suffit d'ajouter l'option "runtime" à votre babelrc après avoir installé babel-runtime avec npm. Et vous pourrez aussi en profiter pour réécrire toutes vos méthodes asynchrones avec async/await.

Hadrien

Hi, I'm a french Javascript Lead Developer, Web Architect from Toulouse, France. I've worked for 12 years for many projects with YUI, AngularJS, Aurelia.io and now React and React native.

Toulouse, France https://hadrien.eu