Rechercher

Async / await


6 Novembre 2019 | 10 mins Mehdi Druon

Après avoir vu en profondeur comment fonctionnent les promesses et comment les utiliser, nous allons nous pencher sur une fonctionnalité arrivée avec l’ES7 : async/await. Async await nous permet de résoudre un problème né de l’utilisation de code asynchrone en javascript : le callback hell, récurrent lorsque l’on cherche à faire de l’asynchrone de manière séquentielle. Async/await n’est globalement que du sucre syntaxique, qui nous permet de lire du code asynchrone comme s’il était synchrone.

⚠️ Il est important de connaître et de comprendre les promesses pour maîtriser async/await. Si ce n’est pas votre cas, je vous conseille de lire mon précédent article qui traite justement des promesses.

Définition

async

Mettre le mot async devant une fonction lui donne l’instruction de retourner une promesse. Si une erreur apparaît pendant l’exécution, la promesse est rejetée. Si la fonction retourne une valeur, la promesse est résolue avec cette valeur. Et si une promesse est retournée, elle reste inchangée.

async function asyncFunction() {
 // équivaut à : return Promise.resolve('résultat');
 return 'résultat';
}

asyncFunction().then(console.log) // "résultat"

les fonctions async rendent le code beaucoup plus concis que l’utilisation de promesses normales :

// Utilisation du then
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()

// Utilisation d'async
const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}

makeRequest()

await

Le mot-clé await ne peut être utilisé qu’à l’intérieur d’une fonction async. Il nous permet d’attendre que la promesse retourne son résultat (ou une erreur) pour continuer l’exécution du code.

async function getAsyncNumber1() // renvoie une promesse avec comme valeur un nombre
async function getAsyncNumber2() // même chose que ligne du dessus

const getAsyncAddition = async () => {
 const number1 = await getAsyncNumber1();
 const number2 = await getAsyncNumber2();
 return number1 + number2;
}

Pour avoir le même résultat sans await, il faudrait

  • soit imbriquer un then() dans un autre :
const getAsyncAddition = () => getAsyncNumber1().then(number1 => getAsyncNumber2().then(number2 => number1 + number2));

Ce qui, vous en conviendrez, est difficile à lire.

  • soit déclarer une variable dans le scope de la fonction :
const getAsyncAddition = () => {
 let number;
 return getAsyncNumber1()
   .then(vnumber => {
     number1 = vnumber1;
     return getAsyncNumber2();
   })
   .then(number2 => number1 + number2);
}

Voici une liste de règles importantes à retenir sur async await :

  • les fonctions async retournent une promesse.
  • les fonctions async utilisent une Promise implicite pour retourner un résultat. Même si une promesse n’est pas retournée explicitement, la fonction async fait en sorte que le code soit passé par une promesse.
  • await bloque l’exécution du code à l’intérieur d’une fonction async. Il permet de s’assurer que la prochaine ligne soit exécutée quand la promesse est résolue. Donc si du code asynchrone est déjà en train de s’exécuter, await n’aura pas d’effet sur lui.
  • il peut y avoir plusieurs await à l’intérieur d’une fonction async.
  • Il faut bien faire attention lors de l’utilisation d’await dans une boucle, car le code peut facilement s’exécuter de manière séquentielle au lieu d’être executé en parallèle.
  • await est toujours utilisé pour une seule promesse.

Pourquoi utiliser async await ?

On pourrait se demander pourquoi utiliser cette fonctionnalité, alors que les promesses existent déjà en JS.

  • Comme vous l’avez vu avec les exemples ci-dessus, async/await nous permet d’avoir un code beaucoup plus concis et nous évite le callback hell.
  • L’utilisation d’async/await permet d’améliorer la gestion d’erreur, car il est possible de gérer les erreurs synchrones et asynchrones en même temps (ce qui n’était pas le cas auparavant), notamment grâce à l’utilisation d’un try/catch.
const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // this parse may fail
        const data = JSON.parse(result)
        console.log(data)
      })
      // décommenter ce bloc pour gérer les erreurs asynchrones
      // .catch((err) => {
      //   console.log(err)
      // })
  } catch (err) {
    console.log(err)
  }
}

Dans cet exemple, le try/catch va casser si le JSON.parse casse car il se passe à l’intérieur d’une promesse. Il est nécessaire d’utiliser un .catch() sur la promesse et donc dupliquer la gestion d’erreur. Voici le même code avec async/await :

const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

Mais il est aussi possible d’utiliser un catch à l’appel de la fonction avec async/await :

const doSomethingAsync = async () => {
    let result = await someAsyncCall();
    return result;
}

doSomethingAsync().
  .then(successHandler)
  .catch(errorHandler);
  • Il arrive assez souvent d’appeler une promesse (qu’on va appeler promesse1) et d’utiliser sa valeur de retour pour appeler une deuxième promesse (qui s’appelle sans surprise promesse2), pour ensuite utiliser le résultat de ces deux promesses pour appeler promesse3. Le code ressemble donc à ceci :

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // Du code
      return promise2(value1)
        .then(value2 => {
          // Du code
          return promise3(value1, value2)
        })
    })
}

Il est possible ici d’englober les promesses 1 et 2 dans un Promise.all pour éviter d’avoir à imbriquer les promesses :

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // Du code
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {
      // Du code
      return promise3(value1, value2)
    })
}

Le problème de cette approche est qu’elle sacrifie la sémantique au profit de la lisibilité. Il n’y a aucune raison de mettre les promesses 1 et 2 dans un tableau, excepté pour éviter l’imbriquation de promesses.

Ce qui est rendu beaucoup plus simple grâce à async/await :

const makeRequest = async () => {
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}
  • Le debugging est rendu beaucoup plus simple grâce à l’utilisation d’async/await. On peut mettre des points d’arrêts contrairement à l’utilisation de promesses.

  • Il est possible d’utiliser await sur des opérations synchrones et asynchrones. Par exemple, écrire await 5 revient à écrire Promise.resolve(5).

Top-level await

Avant de passer à la conclusion, j’aimerais aborder un sujet qui est encore en draft (au stage 3, donc probablement bientôt disponible !) au moment où j’écris ces lignes : le top-level await.

Le top-level await permet aux modules d’agir comme des fonctions async. Il était possible auparavant d’importer un module dans une fonction async, mais un problème se posait : l’export de ce module pouvait être accessible avant que la fonction async soit résolue. Cela pouvait poser problème car si un autre module importe le module présent dans la fonction async, le résultat était potentiellement undefined.

Un article écrit par Rich Harris a cependant mis en avant certaines craintes à propos de cette nouvelle proposition, notamment:

  • Top-level await pourrait bloquer l’exécution du code.
  • Top-level await pourrait bloquer le fetch des ressources.
  • Top-level await pourrait créer des problèmes d’intéropérabilité avec les modules CommonJS.

Pour résoudre ces problématiques, la version en stage 3 propose ces solutions :

  • Top-level await ne bloque pas les exécutions en parralèle.
  • Top-level await se produit durant la phase d’execution du module (qui correspond à la dernière phase, après l’initialization et la configuration), ce qui signifie qu’il se produit après que les ressources soient fetch.
  • Top-level await se limite aux modules, il ne supportera pas les scripts ou les modules CommonJS.

Une solution qui permettrait donc d’utiliser dynamiquement les modules en JavaScript mais qui soulève malgré tout plusieurs questions, car les imports impératifs sont plus lents et mauvais pour les performances d’une application que les imports déclaratifs.

Pour ceux qui aimeraient avoir plus d’informations sur ces deux sujets, voici quelques ressources :

Ces 3 ressources sont par contre en anglais, mais pour les anglophobes, il n’y a pas à s’inquièter car il est très probable que l’on entendra parler à nouveau du top-level await.

Conclusion

async/await est une fonctionnalité incroyable qui permet d’écrire de l’asynchrone facilement. Il est cependant important de comprendre que pour le langage, async/await fonctionne exactement comme une promesse et qu’il ne résoud pas tout les problèmes, comme la gestion de plusieurs appels asynchrones qui sont indépendants. Les fonctions async fonctionnent exactement comme les promesses, mais sont utiles pour gérer les erreurs, éviter d’imbriquer ses promesses, et lire du code asynchrone comme du code synchrone. J’espère que cet article vous aura aidé à y voir plus clair !

Auteur(s)

Mehdi Druon

Jeune développeur JS, j'aime me tenir au courant, et j'aime le piment d'espelette.

Utilisation hors-ligne disponible