🌐 US-Proxy
class="no-icons">
13 février 2022

Async/await

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.

On ne peut pas utiliser await dans les fonctions réguliÚres

Si 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:

  1. Nous devons remplacer les appels .then par await.
  2. Aussi, nous devrions faire la fonction async pour 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.

Les navigateurs modernes permettent l’utilisation de await au niveau supĂ©rieur dans les modules

Dans 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.

Méthodes de classe asynchrones

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/catch

Lorsque 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.all

Lorsque 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:

  1. Fait en sorte qu’elle retourne toujours une promesse.
  2. Permet l’utilisation de await dans celle-ci.

Le mot-clĂ© await devant une promesse fait en sorte que JavaScript attende jusqu’à ce que cette promesse se rĂšgle, puis:

  1. Si c’est une erreur, l’exception est gĂ©nĂ©rĂ©e – comme si throw error Ă©tait appelĂ© Ă  cet endroit prĂ©cis.
  2. 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.

Exercices

Réécrire cet exemple de code du chapitre Chaßnage des promesses en utilisant async/await au lieu de .then/catch:

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new Error(response.status);
      }
    });
}

loadJson('https://javascript.info/no-such-user.json')
  .catch(alert); // Error: 404

Les notes sont en dessous du code:

async function loadJson(url) { // (1)
  let response = await fetch(url); // (2)

  if (response.status == 200) {
    let json = await response.json(); // (3)
    return json;
  }

  throw new Error(response.status);
}

loadJson('https://javascript.info/no-such-user.json')
  .catch(alert); // Error: 404 (4)

Notes:

  1. La fonction loadJson devient async.

  2. Tous les .then intérieurs sont remplacés par await


  3. Nous pouvons return response.json() au lieu de l’attendre, comme ceci:

    if (response.status == 200) {
      return response.json(); // (3)
    }

    Ensuite, le code externe devra “attendre” la rĂ©solution de cette promesse. Dans notre cas, cela n’a pas d’importance.

  4. L’erreur Ă©mise par loadJson est gĂ©rĂ©e par .catch. Nous ne pouvons pas utiliser await loadJson(...) ici, car nous ne sommes pas dans une fonction async


Vous trouverez ci-dessous l’exemple “rethrow”. Réécrivez-le en utilisant async/await au lieu de .then/catch.

Et dĂ©barrassez-vous de la rĂ©cursion en faveur d’une boucle dans demoGithubUser : avec async/await, cela devient facile Ă  faire.

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new HttpError(response);
      }
    });
}

// demander un nom d'utilisateur jusqu'Ă  ce que github renvoie un utilisateur valide
function demoGithubUser() {
  let name = prompt("Enter a name?", "iliakan");

  return loadJson(`https://api.github.com/users/${name}`)
    .then(user => {
      alert(`Full name: ${user.name}.`);
      return user;
    })
    .catch(err => {
      if (err instanceof HttpError && err.response.status == 404) {
        alert("No such user, please reenter.");
        return demoGithubUser();
      } else {
        throw err;
      }
    });
}

demoGithubUser();

Il n’y a pas d’astuces ici. Remplacez simplement .catch par try..catch dans demoGithubUser et ajoutez async/await lĂ  oĂč c’est nĂ©cessaire:

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

async function loadJson(url) {
  let response = await fetch(url);
  if (response.status == 200) {
    return response.json();
  } else {
    throw new HttpError(response);
  }
}

// demander un nom d'utilisateur jusqu'Ă  ce que github renvoie un utilisateur valide
async function demoGithubUser() {

  let user;
  while(true) {
    let name = prompt("Enter a name?", "iliakan");

    try {
      user = await loadJson(`https://api.github.com/users/${name}`);
      break; // pas d'erreur, sortie de la boucle
    } catch(err) {
      if (err instanceof HttpError && err.response.status == 404) {
        // la boucle continue aprĂšs l'alerte
        alert("No such user, please reenter.");
      } else {
        // erreur inconnue, rejeter
        throw err;
      }
    }
  }


  alert(`Full name: ${user.name}.`);
  return user;
}

demoGithubUser();

Nous avons une fonction “normale” appelĂ©e f. Comment pouvez-vous appeler la fonction async wait() et utiliser son rĂ©sultat Ă  l’intĂ©rieur de f ?

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));

  return 10;
}

function f() {
  // ...que devez-vous écrire ici?
  // nous devons appeler async wait() et attendre pour obtenir 10
  // Souvenez-vous, on ne peut pas utiliser "await".
}

P.S. La tĂąche est techniquement trĂšs simple, mais la question est assez courante pour les dĂ©veloppeurs novices en matiĂšre d’async/await.

C’est le cas quand il est utile de savoir comment ça marche Ă  l’intĂ©rieur.

Il suffit de traiter l’appel async comme une promesse et d’y attacher .then:

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));

  return 10;
}

function f() {
  // affiche 10 aprĂšs 1 seconde
  wait().then(result => alert(result));
}

f();
Carte du tutoriel

Commentaires

lire ceci avant de commenter

  • Si vous avez des amĂ©liorations Ă  suggĂ©rer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de prĂ©ciser.
  • Pour insĂ©rer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen
)