Il existe une syntaxe spéciale pour travailler avec les promesses d’une manière plus confortable, appelée “async/await”. Elle est étonnamment facile à comprendre et à utiliser.
Fonctions asynchrones
Commençons par le mot-clé async. Il peut être placé avant une fonction, comme ceci:
async function f() {
return 1;
}
Le mot “async” devant une fonction signifie une chose simple : une fonction renvoie toujours une promesse. Les autres valeurs sont enveloppées dans une promesse résolue automatiquement.
Par exemple, cette fonction renvoie une promesse résolue avec le résultat 1 ; testons-la:
async function f() {
return 1;
}
f().then(alert); // 1
…Nous pourrions explicitement renvoyer une promesse, ce qui reviendrait au même:
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
Ainsi, async s’assure que la fonction renvoie une promesse, et enveloppe les non-promesses dans celle-ci. Assez simple, non ? Mais pas seulement. Il y a un autre mot-clé, await, qui ne fonctionne qu’à l’intérieur des fonctions async, et c’est plutôt cool.
Await
La syntaxe:
// ne fonctionne que dans les fonctions asynchrones
let value = await promise;
Le mot-clé await fait en sorte que JavaScript attende que cette promesse se réalise et renvoie son résultat.
Voici un exemple avec une promesse qui se résout en 1 seconde:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // attendre que la promesse soit résolue (*)
alert(result); // "done!"
}
f();
L’exécution de la fonction fait une “pause” à la ligne (*) et reprend lorsque la promesse s’installe, result devenant son résultat. Ainsi le code ci-dessus affiche “done!” en une seconde.
Soulignons-le : await suspend littéralement l’exécution de la fonction jusqu’à ce que la promesse soit réglée, puis la reprend avec le résultat de la promesse. Cela ne coûte pas de ressources CPU, car le moteur JavaScript peut faire d’autres travaux pendant ce temps : exécuter d’autres scripts, gérer des événements, etc.
C’est juste une syntaxe plus élégante pour obtenir le résultat de la promesse que promise.then. Et c’est plus facile à lire et à écrire.
await dans les fonctions régulièresSi nous essayons d’utiliser await dans une fonction non-async, il y aurait une erreur de syntaxe:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
Nous pouvons obtenir cette erreur si nous oublions de mettre async avant une fonction. Comme indiqué précédemment, await ne fonctionne qu’à l’intérieur d’une fonction async.
Prenons l’exemple showAvatar() du chapitre Chaînage des promesses et réécrivons-le en utilisant async/await:
- Nous devons remplacer les appels
.thenparawait. - Aussi, nous devrions faire la fonction
asyncpour qu’ils fonctionnent.
async function showAvatar() {
// lire notre JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// lire l'utilisateur de github
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// montrer l'avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// attendre 3 secondes
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
Plutôt propre et facile à lire, non ? Bien mieux qu’avant.
await au niveau supérieur dans les modulesDans les navigateurs modernes, await au niveau supérieur fonctionne très bien, lorsque nous sommes dans un module. Nous couvrirons les modules dans l’article Modules, introduction.
Par exemple:
// nous supposons que ce code s'exécute au niveau supérieur, dans un module
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
console.log(user);
Si nous n’utilisons pas de modules, ou des navigateurs plus anciens doivent être supportés, il y a une recette universelle: enveloppement dans une fonction asynchrone anonyme.
Comme ceci :
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
await accepte “thenables”Comme promise.then, await nous permet d’utiliser des objets “thenables” (ceux qui ont une méthode then appelable). L’idée est qu’un objet tiers peut ne pas être une promesse, mais être compatible avec les promesses : s’il supporte .then, c’est suffisant pour l’utiliser avec await…
Voici une classe Thenable de démonstration ; le await ci-dessous accepte ses instances:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// résous this.num*2 après 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// attend pendant 1 seconde, puis le résultat devient 2
let result = await new Thenable(1);
alert(result);
}
f();
Si await reçoit un objet non-promis avec .then, il appelle cette méthode en fournissant les fonctions intégrées resolve et reject comme arguments (comme pour un exécuteur Promise normal). Ensuite, await attend que l’une d’entre elles soit appelée (dans l’exemple ci-dessus, cela se produit à la ligne (*)) et procède ensuite avec le résultat.
Pour déclarer une méthode de classe asynchrone, il suffit de la faire précéder de async:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1 (c'est la même chose que (result => alert(result)))
La signification est la même : elle assure que la valeur retournée est une promesse et active await.
Gestion des erreurs
Si une promesse se résout normalement, alors await promise renvoie le résultat. Mais dans le cas d’un rejet, il jette l’erreur, comme s’il y avait une instruction throw à cette ligne.
Ce code:
async function f() {
await Promise.reject(new Error("Whoops!"));
}
…est le même que celui-ci:
async function f() {
throw new Error("Whoops!");
}
Dans des situations réelles, la promesse peut prendre un certain temps avant de rejeter. Dans ce cas, il y aura un délai avant que await ne lance une erreur.
Nous pouvons rattraper cette erreur en utilisant try..catch, de la même manière qu’un throw normal:
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
En cas d’erreur, le contrôle saute au bloc catch. Nous pouvons également envelopper plusieurs lignes:
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// attrape les erreurs à la fois dans fetch et response.json
alert(err);
}
}
f();
Si nous n’avons pas try..catch, alors la promesse générée par l’appel de la fonction asynchrone f() sera rejetée. Nous pouvons ajouter .catch pour le gérer:
async function f() {
let response = await fetch('http://no-such-url');
}
// f() devient une promesse rejetée
f().catch(alert); // TypeError: failed to fetch // (*)
Si nous oublions d’ajouter .catch à cet endroit, nous obtenons une erreur de promesse non gérée (visible dans la console). Nous pouvons attraper de telles erreurs en utilisant un gestionnaire d’événement global unhandledrejection comme décrit dans le chapitre Gestion des erreurs avec des promesses.
async/await et promise.then/catchLorsque nous utilisons async/await, nous avons rarement besoin de .then, car await gère l’attente pour nous. Et nous pouvons utiliser un try..catch normal au lieu de .catch. C’est généralement (mais pas toujours) plus pratique.
Mais au niveau supérieur du code, lorsque nous sommes en dehors de toute fonction async, nous sommes syntaxiquement incapables d’utiliser await, donc c’est une pratique normale d’ajouter .then/catch pour gérer le résultat final ou l’erreur de chute, comme dans la ligne (*) de l’exemple ci-dessus.
async/await fonctionne bien avec Promise.allLorsque nous devons attendre plusieurs promesses, nous pouvons les envelopper dans Promise.all et ensuite await:
// attendre le tableau de résultats
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
Dans le cas d’une erreur, elle se propage comme d’habitude, de la promesse échouée à Promise.all, et devient alors une exception que nous pouvons attraper en utilisant try..catch autour de l’appel.
Résumé
Le mot-clé async devant une fonction a deux effets:
- Fait en sorte qu’elle retourne toujours une promesse.
- Permet l’utilisation de
awaitdans celle-ci.
Le mot-clé await devant une promesse fait en sorte que JavaScript attende jusqu’à ce que cette promesse se règle, puis:
- Si c’est une erreur, l’exception est générée – comme si
throw errorétait appelé à cet endroit précis. - Sinon, il renvoie le résultat.
Ensemble, ils fournissent un cadre idéal pour écrire du code asynchrone facile à lire et à écrire.
Avec async/await, nous avons rarement besoin d’écrire promise.then/catch, mais nous ne devons pas oublier qu’ils sont basés sur des promesses, parce que parfois (par exemple dans le scope le plus externe) nous devons utiliser ces méthodes. De plus, Promise.all est très utile lorsque l’on attend plusieurs tâches simultanément.
Commentaires
<code>, pour plusieurs lignes – enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)