Programmation asynchrone avec async et await

Si l’introduction des promesses a permis de formaliser le développement de code asynchrone, elle n’a pas permis de résoudre un problème inhérent à l’utilisation des fonctions callback : la difficulté de lecture et donc de maintenance d’un tel code.

Nous pouvons nous inspirer de l’exemple vu au chapitre précédent d’une promesse pour un appel ajax et créer une fonction pour réaliser des appels asynchrones vers un serveur pour récupérer un document JSON :

function getJson(url) {
  return new Promise((resolve, reject) => {
    let request = new XMLHttpRequest();
    request.open("GET", url);

    request.addEventListener("load", function(){
      if(request.status === 200) {
        resolve(JSON.parse(request.responseText));
      } else {
        reject("Erreur du serveur : " + request.status);
      }
    }, false);

    request.addEventListener("error", function(){
      reject("La requête ajax a échoué");
    }, false);

    request.send();
  });
}

Nous pouvons appeler cette fonction :

1
2
3
4
5
6
7
8
getJson("https://monserveur/data.json")
  .then(json => {
    document.getElementById("nom") = json.nom;
    document.getElementById("prenom") = json.prenom;
  })
  .catch(msg => {
    document.getElementById("message") = msg;
  });

Le code ci-dessus est très simple… et pourtant pas si facile à lire que ça. Il faut garder à l’esprit que toutes ces arrow functions passées comme callback ne seront pas forcément appelées. Il serait plus intéressant de pouvoir écrire le même code séquentiellement car il devient plus facile à comprendre.

try {
  const json = getJson("https://monserveur/data.json");
  document.getElementById("nom") = json.nom;
  document.getElementById("prenom") = json.prenom;
} catch(msg) {
  document.getElementById("message") = msg;
}

C’est exactement pour cela que les mots-clés async et await ont été introduits.

Fonction asynchrone

Une fonction peut être déclarée asynchrone grâce au mot clé async. Une fonction asynchrone est une fonction qui appelle une fonction qui retourne une promesse ou une autre fonction asynchrone. Une fonction asynchrone peut appeler une fonction qui retourne une promesse ou une fonction asynchrone en spécifiant le mot-clé await. Si nous reprenons l’exemple de la fonction getJson, elle retourne une promesse. Elle peut donc être appelée dans une fonction asynchrone à l’aide du mot-clé await :

async function maFonctionAsynchrone() {
  const json = await getJson("https://monserveur/data.json");

  // ...
}

Le mot-clé await fonctionne de la manière suivante : il récupère la promesse retournée par la fonction appelée et suspend l’exécution de la fonction asynchrone. Lorsque la promesse est résolue, c’est-à-dire lorsque la fonction resolve est appelée par la promesse, alors l’objet passé en paramètre est retourné par await et le traitement de la fonction continue au point où il avait été suspendu. Si la promesse échoue, c’est-à-dire si c’est la fonction reject qui est appelée par la promesse, alors la valeur passée en paramètre est jetée comme une exception.

Ainsi, grâce aux mots-clés async et await, nous pouvons écrire séquentiellement des traitements qui sont en fait asynchrones. Attention, le mot-clé await ne peut être utilisé que dans le corps d’une fonction asynchrone. Nous allons donc définir une fonction pour réécrire notre traitement :

async function miseAJour() {
  try {
    const json = await getJson("https://monserveur/data.json");
    document.getElementById("nom") = json.nom;
    document.getElementById("prenom") = json.prenom;
  } catch(msg) {
    document.getElementById("message") = msg;
  }
}

miseAJour();

Note

Une fonction asynchrone peut elle-même retourner une valeur. Dans ce cas, la valeur retournée est une promesse. Si ce n’est pas le cas, alors une promesse est créée puis résolue avec la valeur initialement retournée.

Ainsi :

async function foo() {
    return 1;
}

est équivalent à

async function foo() {
    return Promise.resolve(1);
}

Ordre des appels dans un traitement asynchrone

La simplicité de lecture offerte par l’introduction des mots-clés async et await est indéniablement importante. Cependant, il ne faut pas oublier que les appels restent asynchrones et donc que l’ordre d’exécution n’est pas nécessairement celui induit par la lecture du code. Pour nous en convaincre, nous allons prendre un exemple simpliste. Imaginons que nous voulions coder une méthode asynchrone pour réaliser l’addition de deux nombres… ce qui n’a aucun intérêt à part de nous servir d’illustration.

function addition (a, b) {
  return new Promise(resolve => {
    resolve(a + b);
  });
}

La fonction addition retourne une promesse qui se résoudra par la somme des paramètres.

Nous pouvons déclarer une fonction asynchrone qui appelle cette fonction avec await et qui écrit le résultat de l’appel dans la console :

async function calculer() {
  console.log(await addition(2, 3))
}

Si nous appelons cette fonction :

calculer();

La valeur 5 s’affiche dans la console. Mais que se passe-t-il si nous modifions légèrement notre programme de la façon suivante :

console.log("Avant l'appel à calculer");
calculer();
console.log("Après l'appel à calculer");

En exécutant ce code, nous obtenons le résultat suivant dans la console :

Avant l'appel à calculer
Après l'appel à calculer
5

Ce résultat peut être difficile à comprendre. On a l’impression que la fonction calculer n’a pas été appelée au moment attendu. En fait, il faut se souvenir que la fonction calculer est asynchrone et que le modèle d’exécution de l’interpréteur JavaScript est limité à un flux de traitement (mono threaded).

Reprenons le programme : on commence par afficher le message « Avant l’appel à calculer », puis on appelle la fonction calculer. Cette fonction est une fonction asynchrone. Elle appelle la fonction addition avec le mot-clé await. Cette dernière fonction retourne une promesse qui sera appelée de manière asynchrone lorsque l’interpréteur JavaScript aura fini d’exécuter le flux de traitement courant car il ne peut traiter qu’un flux à la fois. Le traitement de la fonction calculer est donc suspendu et on passe à l’étape suivante du programme qui consiste à afficher le message « Après l’appel à calculer ». Comme le programme est maintenant terminé, le flux de traitement est disponible pour exécuter la promesse. Cette dernière est résolue en fournissant le résultat de l’addition et le traitement de la fonction calculer peut enfin reprendre et afficher le résultat de l’addition.

On voit que, même si le programme nous semble écrit de manière séquentielle, l’ordre d’exécution est bien différent. Il faut surtout se rappeler que pour qu’une fonction asynchrone s’exécute, il faut que le traitement courant se termine le plus vite possible pour permettre à l’interpréteur de passer au traitement asynchrone.

La boucle for await

Il existe une variante de l’itération avec for await ... of. Il est ainsi possible de parcourir des tableaux, des chaînes de caractères, des collections de nœuds DOM… de manière asynchrone. Comme pour le mot-clé await, la structure for await ... of ne peut être utilisée que dans une fonction asynchrone.

Exemple d’utilisation de for await
async function afficherNombre(...values) {
  for await (let v of values) {
    console.log(v);
  }
}

Si nous appelons deux fois de suite cette fonction :

afficherNombre(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
afficherNombre(101, 102, 103, 104, 105, 106, 107, 108, 109, 110);

Alors nous verrons bien le comportement asynchrone puisque l’affichage entremêlera les traitements des deux tableaux de valeurs :

Résultat du programme précédent
1
101
2
102
3
103
4
104
5
105
6
106
7
107
8
108
9
109
10
110

Si vous voulez créer votre propre itérable pour l’utiliser avec la structure for await ... of, alors cet objet doit fournir une méthode @@asyncIterator. Cette méthode doit retourner un itérateur déclarant une méthode next.

const premiersNombres =  {
  [Symbol.asyncIterator]() {
    return {
      i: 0,
      next() {
        if (this.i < 10) {
          return {value: this.i++, done: false};
        }
        return {done: true};
      }
    }
  }
}

async function afficherPremiersNombres() {
  for await (let v of premiersNombres) {
    console.log(v);
  }
}

La structure for await ... of accepte également les générateurs. Cela permet d’appeler de manière asynchrone une fonction générative :

function* premiersNombres(max) {
  for (var i = 0; i <= max; i++) {
    yield i;
  }
}

async function afficherPremiersNombres() {
  for await (let v of premiersNombres(10)) {
    console.log(v);
  }
}