Il y a 1 semaine · 7 minutes · Front

async/await, une meilleure façon de faire de l’asynchronisme en JavaScript

Enfin disponible pour tous

Avec le début de la LTS de Node.js en v8 ce 31 octobre, c’est l’occasion de revenir sur ce qui est sans doute la fonctionnalité la plus importante depuis la v6 : le async/await.

Disponible depuis la v7.0 de Node.js au travers d’un flag, cette fonctionnalité était cependant déconseillée en production en raison d’une fuite mémoire. C’est avec la version 5.5 de V8 intégrée dans la v7.6 de Node.js que son support devient complet.

async/await est aussi supporté par tous les navigateurs evergreen et est depuis longtemps utilisable via babel.

Qu’est-ce que async/await ?

Rappelons au préalable que par design, Node.js est asynchrone. Le standard est basé sur des callbacks, qui ont vite été supplantées par les promesses, une manière plus élégante et efficace d’écrire du code asynchrone. Il en va de même pour le JavaScript côté client qui se base sur des évènements.

Cependant, l’utilisation de promesses mêne souvent à du code verbeux et qui s’éloigne des pratiques habituelles. Qui n’a jamais rêvé de lire et d’écrire du code asynchrone comme s’il travaillait avec du code synchrone ?  async/await devrait faire le bonheur de tous ces développeurs.

async

Une fonction définie avec le mot clé async renvoie systématiquement une promesse : si une erreur est levée pendant l’exécution de la fonction, la promesse est rejetée, et si une valeur est retournée, la promesse est résolue avec cette valeur. Si une promesse est retournée, elle est inchangée.

async function fonctionAsynchroneOk() {
 // équivaut à :
 // return Promise.resolve('résultat');
 return 'résultat';
}
fonctionAsynchroneOk().then(console.log) // log "résultat"

async function fonctionAsynchroneKo() {
 // équivaut à :
 // return Promise.reject(new Error('erreur'));
 throw new Error('erreur');
}
fonctionAsynchroneKo().catch(err => console.log(err.message)) // log "erreur"

Ce comportement est comparable aux callbacks de résolution et de rejet de promesses (les fonctions dans le .then/.catch), qui de la même façon encapsulent automatiquement les valeurs de retour et les erreurs dans une promesse.

Retourner systématiquement une promesse rejetée lorsqu’une erreur est levée est un avantage indéniable qui justifie à lui seul l’utilisation du mot clé async, comme nous allons en parler par la suite. C’est aussi une excellente manière de documenter son code.

await

La partie la plus intéressante est l’utilisation du mot clé await, qui ne peut être utilisé que dans une fonction async. Il permet d’attendre la résolution d’une promesse et retourner sa valeur.

async function getNombreAsynchrone1() {/* traitement asynchrone (e.g. appel d’une API HTTP) */}
async function getNombreAsynchrone2() {/* traitement asynchrone (e.g. appel d’une API HTTP) */}

async function getAdditionAsynchrone() {
 const nombre1 = await getNombreAsynchrone1();
 const nombre2 = await getNombreAsynchrone2();
 return nombre1 + nombre2;
}

Si on veut le même résultat avec une utilisation “classique” des promesses, on peut par exemple :

  • casser la chaîne de promesses avec un then imbriqué
function getAdditionAsynchrone() {
 return getNombreAsynchrone1()
   .then(nombre1 => {
     return getNombreAsynchrone2()
       .then(nombre2 => nombre1 + nombre2);
   });
}
  • ou encore déclarer une variable dans le scope de la fonction
function getAdditionAsynchrone() {
 let nombre1;
 return getNombreAsynchrone1()
   .then(vnombre1 => {
     nombre1 = vnombre1;
     return getNombreAsynchrone2();
   })
   .then(nombre2 => nombre1 + nombre2);
}

Comme vous pouvez le voir, ces équivalents sont beaucoup moins clairs et élégants que la version avec async/await.

Le mot clé await peut aussi être utilisé devant une valeur, et n’a alors aucun effet, ce qui peut être utile dans le cas d’un refactoring car on peut interchanger du code synchrone et asynchrone sans modifier le code appelant ; ce n’est cependant pas conseillé.

function getNombreSynchone() {/* traitement synchrone */}

async function getAdditionAsynchrone() {
 const nombre1 = await getNombreSynchone();
 const nombre2 = await 10;
 return nombre1 + nombre2;
}

Parallélisation

Le premier exemple peut être modifié pour permettre la parallélisation de traitements asynchrones via l’habituel Promise.all ; rien de nouveau là-dessus.

async function getAdditionAsynchroneParallele() {
 const [nombre1, nombre2] = await Promise.all([
   getNombreAsynchrone1(),
   getNombreAsynchrone2(),
 ]);
 return nombre1 + nombre2;
}

Une meilleure gestion des erreurs

Si vous êtes familier avec les promesses, vous savez déjà que, lorsqu’une promesse est rejetée, le traitement de l’erreur s’effectue dans le .catch. Quand une fonction contient à la fois du code synchrone et asynchrone, la gestion d’erreur se retrouve souvent dupliquée.

function appelSynchrone() { /** traitement synchrone */}
function appelAsynchrone() { /** traitement asynchrone */ }
function traitementAppel() { /** traitement synchrone ou asynchrone */ }

function run() {
 try {
   appelSynchrone();
   return appelAsynchrone()
     .then(traitementAppel)
     .catch(e => {
       console.log('Error', e);
     });
 } catch (e) {
   console.log('Error', e);
 }
}

Les traitements d’erreurs synchrone (dans le try/catch) et asynchrone (dans le .catch) sont dupliqués. On pourrait factoriser le traitement dans une fonction, mais l’appel se ferait toujours à deux endroits.

async/await répond à cette problématique en centralisant le code dans les blocs try/catch.

function appelSynchrone() { /** traitement synchrone */}
async function appelAsynchrone() { /** traitement asynchrone */ }
function traitementAppel() { /** traitement synchrone ou asynchrone */ }

async function run() {
 try {
   appelSynchrone();
   const result = await appelAsynchrone();
   return traitementAppel(result);
 } catch (e) {
   console.log('Error', e);
 }
}

Une autre problématique réglée par async/await est ici l’écriture de notre fonction appelAsynchrone : comment gérer les éventuelles erreurs levées de manière synchrone ?

function appelAsynchrone() {
 /** bloc synchrone */
 console.log('synchrone');
 throw new Error('oups'); // erreur synchrone, ce qui casse notre API et les éventuels .then/.catch de l'appelant
 return blocAsynchrone();
}

Une erreur peut se glisser dans la partie synchrone, levant une exception ce qui ne respecte pas l’API de notre fonction qui est sensée retourner une promesse. La façon habituelle de contourner ce problème est de toujours commencer une fonction asynchrone par Promise.resolve :

function appelAsynchrone() {
 return Promise.resolve()
   .then(() => {
     console.log('sychrone');
     throw new Error('oups'); // l’erreur est correctement encapsulée
   })
   .then(blocAsynchrone);
}

Grâce au mot clé async, ce n’est plus la peine de polluer ainsi notre code, une fonction async renvoyant systématiquement une promesse :

async function appelAsynchrone() {
 /** bloc synchrone */
 console.log('synchrone');
 throw new Error('oups'); // l’erreur est correctement encapsulée grâce au mot clé async
 return blocAsynchrone();
}

L’utilisation des blocs try/catch permet également de pallier une limite des promesses natives : l’utilisation possible du bloc finally, qui n’a pas encore d’équivalent (mais qui est une proposition en stage 3 d’ECMAScript à l’écriture de cet article).

Migrer vers async/await

Si vous travaillez déjà avec des promesses, la migration est plutôt triviale : rajoutez le mot clé async aux fonctions retournant une promesse, remplacez les .then par des await, et les .catch par des blocs try/catch. Le code sera plus clair et concis.

Si vous travaillez avec des callbacks, c’est le même travail que pour migrer vers une utilisation de promesses classique : il faut promessifier vos fonctions. Historiquement on se tournait vers Bluebird, mais depuis sa v8.0 Node.js propose une fonction promisify dans le module util.

const { promisify } = require('util');
const fs = require('fs');
const readFileAsync = promisify(fs.readFile);

async function lireMonFichier() {
 try {
   const texteDuFichier = await readFileAsync('path/du/fichier', { encoding: 'utf8' });
   console.log('contenu :', texteDuFichier);
 } catch (err) {
   console.log('erreur :', err);
 }
}

 Conclusion 

Comme nous l’avons vu, async/await permet d’écrire du code asynchrone de manière très efficace, et rend obsolète l’utilisation des habituels .then/.catch.

Cette fonctionnalité étant supportée par Node.js et les navigateurs evergreen, ou si besoin utilisable à l’aide de babel, ça serait dommage de s’en priver.

Anthony Giniers
Anthony est développeur fullstack, Javaiste repenti spécialisé dans les technologies JavaScript et les problématiques liées au Cloud.

Retrouvez le sur Twitter : @aginiers

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *