Question 1Que renvoie invoices.filter((invoice) => invoice.id === "B-302") ?
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
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éthode | Renvoie | Attention |
|---|---|---|
| map | Nouveau tableau (même taille) | Le tableau d'origine ne change pas |
| filter | Nouveau tableau (filtré) | Un tableau vide si rien ne correspond |
| find / findIndex | Un élément / un index | undefined / -1 si rien n'est trouvé |
| some / every | Un booléen | false / true sur un tableau vide |
| reduce | Le dernier retour du callback | Un tableau vide exige une valeur initiale |
| sort / toSorted | Un 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')
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
- Appelle
later("photo-17.jpg", (fileName) => { … })
- Le premier résultat arrive dans
fileName - Le deuxième
laterest appelé ici
- Le deuxième résultat arrive dans
thumbnail - L'affichage va dans la fonction la plus interne
- Part de
delay(100, "photo-17.jpg")
- Reçoit
fileNameet renvoie ledelaysuivant
- Reçoit
thumbnailet l'affiche
const fileName = await delay(…)const thumbnail = await delay(…)console.log(…)s'écrit à la ligne suivante
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.
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)
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 écris | Rôle | Attention |
|---|---|---|
| try / catch | Capte les exceptions du bloc | Un catch vide masque les échecs |
| finally | Toujours exécuté en dernier | Regroupe le nettoyage ici |
| throw new Error | Lève une exception à cet endroit | new Error seul n'arrête rien |
| class extends Error | Type repérable par instanceof | Teste tes propres types avant Error |
| Le cause d'une Error | Joint l'exception d'origine | Se lit avec error.cause |
| Le .catch d'une Promise | Appelé au rejet de la Promise | À la fin de la chaîne de then |
Vérification des connaissances
Répondez à chaque question une par une.
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 ?