Il y a 5 ans -
Temps de lecture 7 minutes
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.
[js gutter= »true »]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"[/js]
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.
[js gutter= »true »]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;
}[/js]
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é
[js gutter= »true »]function getAdditionAsynchrone() {
return getNombreAsynchrone1()
.then(nombre1 => {
return getNombreAsynchrone2()
.then(nombre2 => nombre1 + nombre2);
});
}[/js]
- ou encore déclarer une variable dans le scope de la fonction
[js gutter= »true »]function getAdditionAsynchrone() {
let nombre1;
return getNombreAsynchrone1()
.then(vnombre1 => {
nombre1 = vnombre1;
return getNombreAsynchrone2();
})
.then(nombre2 => nombre1 + nombre2);
}[/js]
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é.
[js gutter= »true »]function getNombreSynchone() {/* traitement synchrone */}
async function getAdditionAsynchrone() {
const nombre1 = await getNombreSynchone();
const nombre2 = await 10;
return nombre1 + nombre2;
}[/js]
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.
[js gutter= »true »]async function getAdditionAsynchroneParallele() {
const [nombre1, nombre2] = await Promise.all([
getNombreAsynchrone1(),
getNombreAsynchrone2(),
]);
return nombre1 + nombre2;
}[/js]
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.
[js gutter= »true »]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);
}
}[/js]
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
.
[js gutter= »true »]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);
}
}[/js]
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 ?
[js gutter= »true »]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();
}[/js]
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
:
[js gutter= »true »]function appelAsynchrone() {
return Promise.resolve()
.then(() => {
console.log(‘sychrone’);
throw new Error(‘oups’); // l’erreur est correctement encapsulée
})
.then(blocAsynchrone);
}[/js]
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 :
[js gutter= »true »]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();
}[/js]
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
.
[js gutter= »true »]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);
}
}[/js]
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.
Commentaire
8 réponses pour " async/await, une meilleure façon de faire de l’asynchronisme en JavaScript "
Published by Glen LB , Il y a 4 ans
Merci pour cet article clair sur les async/await, pas forcément évidents à comprendre au premier abord ^^.
Published by Emule , Il y a 4 ans
Très claire l’article ! J’ai mieux compris ce qu’il fallait comprendre.
Published by ftz , Il y a 4 ans
Merci pour cette petite explication.
Published by Pierre Billard , Il y a 3 ans
J’ai bien fait de chercher sur Google, c’est exactement ce qu’il me fallait. Merci beaucoup pour les explications très détaillées.
Published by volkastream , Il y a 3 ans
Merci, pour cet article si très claire a comprendre
Published by Diagrim , Il y a 3 ans
Merci, pour l’explication simple et claire.
Super
Published by Prendenmain , Il y a 2 ans
très bien expliquez, Merci
Published by Samantha RONDESSE , Il y a 1 an
Je comprends maintenant et j’y vois plus claire, merci pour votre éclaircissement !!! Vous êtes profond.