Récapitulatif de syntaxe — méthodes de tableau, asynchrone et exceptions

Des tableaux récapitulatifs : les méthodes de tableau selon ce qu'elles renvoient, callbacks, then ou await, et où chaque échec est intercepté.

Additionner une liste de factures, attendre la fin d'un envoi de fichier, intercepter un échec pour afficher un message : chacune de ces tâches peut s'écrire de plusieurs façons. Si tu choisis la mauvaise, tu risques de recevoir undefined là où tu attendais un tableau, ou un échec qui n'atteint aucun catch.

Cet article montre quelle méthode de tableau utiliser selon le cas, quelle façon d'attendre du code asynchrone choisir et comment intercepter les exceptions.

Choisir selon le résultat voulu — ce que renvoient les méthodes de tableau

Imagine que tu veux tirer trois informations d'une liste de factures : le nombre de factures payées, la première facture impayée et le montant total. Les méthodes de tableau s'appellent toutes de la même façon, avec un callback, donc rien qu'en regardant l'appel, tu ne peux pas savoir si tu vas récupérer un tableau ou une seule valeur.

Ici, les méthodes sont classées selon la forme de leur valeur de retour : un nouveau tableau, l'un des éléments du tableau, ou une valeur calculée à partir du tableau, comme un booléen ou un total. Une fois la forme connue, tu sais aussi si tu peux appeler join sur le résultat ou lire une propriété comme .id.

const invoices = [
  { id: "B-301", amount: 4800, paid: true },
  { id: "B-302", amount: 1200, paid: false },
  { id: "B-303", amount: 3600, paid: true },
  { id: "B-304", amount: 2500, paid: false },
];

// Renvoie un tableau : map garde le même nombre d'éléments, filter ne garde que ceux qui correspondent
console.log(invoices.map((invoice) => invoice.id).join(", "));   // B-301, B-302, B-303, B-304
console.log(invoices.filter((invoice) => invoice.paid).length);  // 2

// Renvoie l'élément lui-même : le premier qui correspond
console.log(invoices.find((invoice) => !invoice.paid).id);  // B-302

// Renvoie une valeur calculée à partir du tableau : some donne un booléen, reduce la valeur accumulée
console.log(invoices.some((invoice) => invoice.amount >= 4000));              // true
console.log(invoices.reduce((total, invoice) => total + invoice.amount, 0));  // 12100
Un même tableau, des formes de retour différentes
invoices4 factures.filterteste paid.findteste !paid.reduceadditionne amountTableau de 2B-301 et B-303Un élémentfacture B-302Un nombre12100Puis.map ou .joinPuis.id ou .amountDirectement dansconsole.log
Le résultat est un tableau, un élément ou un nombre, et ce que tu peux écrire ensuite en dépend. Une fois que tu sais quelle forme de résultat tu veux, le choix de la méthode se resserre.

Si tu utilises filter alors que tu ne veux qu'un seul élément, tu récupères un tableau qui contient cet élément, et tu dois ajouter [0] avant de lire .id. Le tableau ci-dessous classe selon leur valeur de retour les méthodes de tableau à callback vues dans ce cours.

MéthodeRenvoieAttention
mapNouveau tableau (même taille)Le tableau d'origine ne change pas
filterNouveau tableau (filtré)Un tableau vide si rien ne correspond
find / findIndexUn élément / un indexundefined / -1 si rien n'est trouvé
some / everyUn booléenfalse / true sur un tableau vide
reduceLe dernier retour du callbackUn tableau vide exige une valeur initiale
sort / toSortedUn tableau triésort modifie aussi le tableau d'origine

Savoir quand on peut enchaîner — chaînes de méthodes et forEach

Imagine que tu veux le total des seules factures impayées. Si tu stockes le résultat de filter dans une variable, que tu appelles map sur cette variable, puis reduce, tu te retrouves à nommer des tableaux intermédiaires qui ne resserviront jamais. Et si tu essaies de tout écrire en une seule expression sans savoir quelles méthodes peuvent se suivre, tu tomberas sur une TypeError à l'endroit où deux d'entre elles se rejoignent.

Dans un chaînage (appeler la méthode suivante sur la valeur renvoyée par la précédente), chaque appel comme .filter(...) compte pour une étape, et chaque étape est appelée sur ce qu'a renvoyé l'étape d'avant. Tu ne peux continuer à appeler des méthodes de tableau que si l'étape précédente a renvoyé un tableau.

// invoices contient les mêmes 4 factures que dans la section précédente

// Tant qu'un tableau revient, tu peux continuer à ajouter des méthodes de tableau
const unpaidTotal = invoices
  .filter((invoice) => !invoice.paid)             // Tableau : les 2 factures B-302 et B-304
  .map((invoice) => invoice.amount)               // Tableau : [1200, 2500]
  .reduce((total, amount) => total + amount, 0);  // Nombre : 3700
console.log(unpaidTotal);                         // 3700

// toSorted renvoie aussi un tableau, donc tu peux enchaîner jusqu'à map et join
const idsByAmount = invoices.toSorted((a, b) => b.amount - a.amount).map((invoice) => invoice.id);
console.log(idsByAmount.join(", "));  // B-301, B-303, B-304, B-302

// forEach renvoie undefined, donc tout ce qui est enchaîné après échoue
invoices.forEach((invoice) => console.log(invoice.id)).map((invoice) => invoice.id);
// Après avoir affiché B-301 à B-304
// TypeError: Cannot read properties of undefined (reading 'map')
On ne peut enchaîner que tant qu'un tableau revient
invoicestableau de 4.filter(...)tableau de 2.map(...)[1200, 2500].reduce(...)3700invoicestableau de 4.forEach(...)undefined.map(...)sur undefinedS'arrête surune TypeError
Dans la rangée du haut, chaque étape renvoie un tableau jusqu'à ce que reduce renvoie un nombre. forEach renvoie undefined, donc la chaîne casse à l'étape suivante.

Place en fin de chaîne toute étape qui ne renvoie pas de tableau. Après find, tu peux lire une propriété comme .id, mais pas appeler map, et le nombre renvoyé par reduce se range simplement dans une variable.

Appeler map après find donne un autre message

Si tu enchaînes .map(...) sur l'élément renvoyé par find ou sur le nombre renvoyé par reduce, l'exécution s'arrête sur une TypeError du type invoices.find(...).map is not a function. Le message n'est pas le même qu'après forEach, mais dans les deux cas, l'étape juste avant .map n'a pas renvoyé de tableau.

Comparer les façons d'attendre — callbacks / then / await

Imagine que tu envoies une photo, puis que tu en crées une miniature. Ces deux opérations prennent du temps avant de donner leur résultat : les fonctions ne peuvent donc pas le renvoyer directement, et la deuxième étape doit attendre que le résultat de la première soit arrivé.

Dans le code ci-dessous, ① utilise le style callback (tu passes à la fonction qui lance le travail une seconde fonction, qu'elle appelle une fois le travail terminé ; aucune Promise n'est renvoyée), qui est aussi le fonctionnement de setTimeout. ② passe une fonction au then de la Promise obtenue, et ③ utilise await dans une fonction async.

// ① Style callback : on passe une fonction à appeler une fois terminé, et on écrit l'étape suivante dedans
// later appelle callback avec value au bout de 100ms (elle ne renvoie pas de Promise)
function later(value, callback) {
  setTimeout(() => callback(value), 100);
}
later("photo-17.jpg", (fileName) => {
  later(`Miniature de ${fileName}`, (thumbnail) => {
    console.log(`① ${thumbnail} créée`);  // ① Miniature de photo-17.jpg créée
  });
});

// ② then : on relie l'étape suivante avec .then (delay est la même que dans l'article sur les Promises)
delay(100, "photo-17.jpg")
  .then((fileName) => delay(100, `Miniature de ${fileName}`))
  .then((thumbnail) => console.log(`② ${thumbnail} créée`));  // ② Miniature de photo-17.jpg créée

// ③ await : on écrit l'étape suivante à la ligne d'en dessous et on récupère le résultat dans une variable (suppose une exécution dans une fonction async, comme dans l'article précédent)
const fileName = await delay(100, "photo-17.jpg");
const thumbnail = await delay(100, `Miniature de ${fileName}`);
console.log(`③ ${thumbnail} créée`);  // ③ Miniature de photo-17.jpg créée
À quelle profondeur s'écrit chaque étape suivante
① Style callback — passer une fonction à later
  • Appelle later("photo-17.jpg", (fileName) => { … })
Dans (fileName) => { … }
  • Le premier résultat arrive dans fileName
  • Le deuxième later est appelé ici
Dans (thumbnail) => { … }
  • Le deuxième résultat arrive dans thumbnail
  • L'affichage va dans la fonction la plus interne
② then — passer une fonction à .then
  • Part de delay(100, "photo-17.jpg")
Dans la fonction passée au premier .then
  • Reçoit fileName et renvoie le delay suivant
Dans la fonction passée au deuxième .then
  • Reçoit thumbnail et l'affiche
③ await — récupérer le résultat dans une variable
  • const fileName = await delay(…)
  • const thumbnail = await delay(…)
  • console.log(…) s'écrit à la ligne suivante
Seul le style callback de ① place la fonction suivante à l'intérieur de celle qui reçoit le résultat. L'await de ③ ne crée aucune fonction : les étapes sont de simples lignes, de haut en bas.

La fonction du deuxième then ne reçoit que thumbnail : pour y utiliser aussi fileName, il faudrait le recopier dans une variable extérieure. Avec await, fileName est dans une variable que toutes les lignes en dessous peuvent lire. Le tableau ci-dessous compare les trois styles.

StyleOù arrive le résultatQuand tu ajoutes des étapes
Style callbackArgument du callbackUn niveau de plus par étape
Passer une fonction à thenArgument du callback de thenEnchaînées à même profondeur
Attendre avec awaitLa variable à gaucheUne étape par ligne

Vérifier où aboutit un échec — try / catch et .catch

Imagine une fonction qui réserve du stock et qui échoue quand la quantité commandée dépasse le stock disponible. Pour une fonction ordinaire qui ne renvoie pas de Promise, il suffit d'entourer la ligne d'appel d'un try / catch pour intercepter l'échec. Mais l'échec d'une fonction qui renvoie une Promise atteint le bloc catch ou non selon la façon dont tu écris l'appel.

Le bloc catch qui suit try reçoit les exceptions levées dans les lignes qu'il entoure. Le .catch d'une Promise appelle la fonction que tu lui as passée quand cette Promise devient rejected.

function checkStock(count) {
  if (count > 3) {
    throw new Error(`Stock insuffisant : ${count} articles`);
  }
}
async function reserveStock(count) {
  await delay(100);   // delay est la même que dans l'article sur les Promises
  checkStock(count);  // Un throw ici rend rejected la Promise renvoyée par cette fonction
}

// ① Une fonction qui ne renvoie pas de Promise : on entoure la ligne d'appel d'un try
try { checkStock(5); } catch (error) { console.log(`① ${error.message}`); }
// ② Le style then : on passe à .catch une fonction qui reçoit l'erreur
reserveStock(5).catch((error) => console.log(`② ${error.message}`));
// ③ Le style await : on entoure la ligne await d'un try
try { await reserveStock(5); } catch (error) { console.log(`③ ${error.message}`); }

// ① Stock insuffisant : 5 articles
// ② Stock insuffisant : 5 articles (après 100ms)
// ③ Stock insuffisant : 5 articles (après 100ms)
Trois endroits qui reçoivent le même échec
checkStock throwau-delà de 3① checkStock(5)appel direct② reserveStock(5).catch(...)③ awaitreserveStock(5)La ligne d'appellève une exceptionPromise renvoyéerejectedLa ligne awaitlève une exceptionBloc catchaprès le tryArgument de lafonction du .catchBloc catchaprès le try
En ②, la ligne d'appel ne lève pas d'exception : c'est la Promise renvoyée qui devient rejected. Un bloc catch ne reçoit que ① et ③, où la ligne elle-même lève l'exception.

Que tu la reçoives avec .catch ou avec un bloc catch, c'est la même Error qui arrive, donc error.message et les tests avec instanceof fonctionnent dans les deux cas. Le tableau ci-dessous récapitule les façons de gérer les exceptions vues dans ce cours.

Ce que tu écrisRôleAttention
try / catchCapte les exceptions du blocUn catch vide masque les échecs
finallyToujours exécuté en dernierRegroupe le nettoyage ici
throw new ErrorLève une exception à cet endroitnew Error seul n'arrête rien
class extends ErrorType repérable par instanceofTeste tes propres types avant Error
Le cause d'une ErrorJoint l'exception d'origineSe lit avec error.cause
Le .catch d'une PromiseAppelé au rejet de la PromiseÀ la fin de la chaîne de then
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Que renvoie invoices.filter((invoice) => invoice.id === "B-302") ?

Question 2Dans invoices.filter(...).map(...).reduce(...), quel tableau reduce parcourt-il ?

Question 3Quand tu enchaînes trois étapes asynchrones en style callback, où écris-tu la troisième étape ?