Promise.all() : lancer ensemble ce qui est réellement indépendant
Lancez ensemble les opérations réellement indépendantes, et sachez ce qui se passe quand l’une d’elles échoue.
Où en êtes-vous avec cette notion ?
Une indication personnelle, enregistrée uniquement dans ce navigateur.
À lire ensuite
Le format café
L’essentiel à comprendre, le temps d’un café.
Envie de creuser ? Un cours plus complet vous attend juste après, à déplier sans quitter cette page.
Ce que vous allez comprendre
Quand plusieurs attentes peuvent partir ensemble, quand une dépendance réelle l’interdit, et ce que Promise.all() fait en cas d’échec.
Trois attentes qui s’additionnent
const user = await getUser();
const orders = await getOrders();
const notifications = await getNotifications();
Ce code fonctionne, mais chaque ligne attend que la précédente soit terminée. Si chaque opération demandait environ cinq cents millisecondes, l’attente totale approcherait une seconde et demie. Ces durées sont illustratives : elles servent à raisonner sur l’enchaînement, elles ne proviennent d’aucune mesure.
Les lancer ensemble
const [user, orders, notifications] = await Promise.all([
getUser(),
getOrders(),
getNotifications(),
]);
await successifs
A ──────►
B ──────►
C ──────►
Promise.all
A ──────►
B ──────►
C ──────►
Les trois opérations partent quasiment en même temps, et Promise.all() attend que toutes soient terminées. Les résultats reviennent dans l’ordre du tableau fourni, pas dans leur ordre d’arrivée : c’est ce qui rend la lecture par déstructuration fiable.
Quand la dépendance est réelle
const user = await getUser();
const orders = await getOrders(user.id);
Ici getOrders a besoin de user.id, qui n’existe pas avant la première attente. Aucune parallélisation n’est possible. La question à se poser devant deux await successifs est donc simple : la seconde opération a-t-elle réellement besoin du résultat de la première ?
Tout ou rien
Si l’une des Promises rejette, Promise.all() rejette : vous passez dans le catch et ne récupérez aucun des résultats réussis. Lorsque vous voulez au contraire connaître l’issue de chacune, utilisez Promise.allSettled(), qui renvoie pour chaque entrée un objet { status: 'fulfilled', value } ou { status: 'rejected', reason }.
Deux réflexes
Un await placé dans une boucle sérialise les opérations. Si elles sont indépendantes, Promise.all(users.map(envoyer)) les lance ensemble. Mais avec une liste énorme, ne lancez pas cent mille opérations d’un coup : passez par des lots ou une limite de concurrence.
Fiche mémo
| Situation | Réponse |
|---|---|
| Opérations indépendantes | Promise.all() |
| Une dépend du résultat de l’autre | await successifs |
| Il faut l’issue de chacune | Promise.allSettled() |
| Liste très longue | Lots ou limite de concurrence |
L’idée à emporter : Promise.all() ne rend rien parallélisable ; il supprime seulement des attentes qui n’avaient pas lieu d’être.
Et si on allait plus loin ?
Le café vous a donné les repères. Prenez maintenant le temps de comprendre les mécanismes et de pratiquer, si vous le souhaitez.
Aller plus loinParalléliser sans perdre le contrôle des erreursDévelopper le coursReplier le cours
Ce que vous saurez faire
Reconnaître ce qui est réellement indépendant, lancer plusieurs opérations ensemble sans perdre la gestion des erreurs, et limiter la concurrence quand la liste s’allonge.
Les exemples n’utilisent que des minuteurs : aucun serveur, aucune base de données. Ils se collent dans la console d’un navigateur, ou s’exécutent avec Node.js dans un module ECMAScript (fichier .mjs), car ils emploient await au niveau supérieur. Dans un fichier .js classique, enveloppez-les dans une fonction async. Les durées annoncées sont illustratives : elles servent à raisonner sur l’enchaînement, pas à mesurer une performance.
1. Créer la Promise, c’est déjà lancer le travail
Promise.all() ne lance rien. Ce sont les appels de fonction placés dans le tableau qui démarrent les opérations ; Promise.all() se contente d’attendre leur issue.
const tache = (nom, ms) =>
new Promise((resolve) => setTimeout(() => resolve(nom), ms));
console.time('ensemble');
const resultats = await Promise.all([
tache('a', 300),
tache('b', 200),
tache('c', 100),
]);
console.timeEnd('ensemble');
console.log(resultats);
Résultat attendu : une durée proche de la plus longue des trois attentes, et le tableau [ 'a', 'b', 'c' ]. Deux enseignements. D’abord, l’ordre du tableau de résultats suit celui des entrées, jamais l’ordre d’arrivée. Ensuite, si vous aviez écrit await tache('a', 300) avant de construire le tableau, les attentes auraient recommencé à s’additionner : tout le gain vient du moment où les opérations démarrent.
Référence : Promise.all().
2. Un rejet arrête l’attente, pas les opérations
Dès qu’une entrée rejette, Promise.all() rejette avec cette raison, sans attendre les autres. Mais les autres opérations, elles, continuent : rien ne les annule.
const echoue = (ms) =>
new Promise((_, reject) => setTimeout(() => reject(new Error('KO')), ms));
const reussit = (ms) =>
new Promise((resolve) => setTimeout(() => resolve('OK'), ms));
try {
await Promise.all([echoue(100), reussit(500)]);
} catch (error) {
console.error('échec global :', error.message);
}
Résultat attendu : le message d’échec apparaît vers cent millisecondes, alors que la seconde opération tourne encore et ne se terminera que vers cinq cents. Si cette seconde opération avait rejeté après le rejet global, personne n’aurait traité son rejet : c’est une cause classique de rejet non géré. Pour interrompre réellement une requête réseau, il faut un mécanisme d’annulation explicite, par exemple un AbortController passé à fetch().
3. Chaque combinateur répond à une question différente
Promise.allSettled() attend toutes les entrées et ne rejette pas : il renvoie un tableau décrivant chaque issue.
const issues = await Promise.allSettled([echoue(100), reussit(200)]);
console.log(issues);
// [ { status: 'rejected', reason: Error: KO },
// { status: 'fulfilled', value: 'OK' } ]
C’est le bon choix quand un échec partiel reste acceptable : un tableau de bord peut afficher trois blocs sur quatre et signaler clairement le quatrième.
Deux autres combinateurs existent. Promise.any() renvoie la première entrée tenue et n’échoue que si toutes rejettent, ce qui convient pour interroger plusieurs sources équivalentes. Promise.race() renvoie la première issue quelle qu’elle soit, succès ou échec, et sert typiquement à imposer un délai maximal.
Référence : Promise.allSettled().
Pièges et limites
- Paralléliser une vraie dépendance.
getOrders(user.id)a besoin deuser. Aucun combinateur ne contourne cet enchaînement ; il faut deux attentes successives. - Croire que
Promise.all()annule. Le premier rejet libère l’attente ; les opérations déjà parties continuent, avec leurs effets et leurs éventuels rejets orphelins. - Ouvrir cent mille opérations d’un coup.
Promise.all(liste.map(envoyer))sur une liste énorme sature la mémoire, le réseau ou le service distant. Traitez par lots, ou avec une limite de concurrence. - Confondre parallélisme et concurrence. JavaScript n’exécute pas plusieurs morceaux de votre code au même instant : ce sont les attentes qui se recouvrent. Trois calculs synchrones lourds ne gagneront donc rien.
À vous de jouer
Vous devez envoyer un message à une liste d’identifiants, avec au maximum trois envois en cours simultanément, et connaître l’issue de chacun sans qu’un échec interrompe le reste. Écrivez cette fonction en n’utilisant que Promise.allSettled() et un découpage en lots. Simulez l’envoi par un minuteur qui rejette pour un identifiant donné, et vérifiez que les envois suivants ont bien lieu.
Correction commentée
const envoyer = (id) =>
new Promise((resolve, reject) =>
setTimeout(() => (id === 3 ? reject(new Error('id 3')) : resolve(id)), 100),
);
async function envoyerParLots(ids, taille) {
const issues = [];
for (let i = 0; i < ids.length; i += taille) {
const lot = ids.slice(i, i + taille);
issues.push(...(await Promise.allSettled(lot.map(envoyer))));
}
return issues;
}
envoyerParLots([1, 2, 3, 4, 5], 3).then((issues) => console.log(issues));
Résultat attendu : cinq objets, dont un seul { status: 'rejected', reason: Error: id 3 }, les quatre autres portant status: 'fulfilled'.
Pourquoi c’est la bonne réponse. Le await dans la boucle est ici volontaire : il sépare les lots, et c’est précisément la limite de concurrence demandée. À l’intérieur d’un lot, lot.map(envoyer) lance les trois envois ensemble, car la Promise est créée au moment de l’appel. Promise.allSettled() garantit qu’un échec ne fait tomber ni le lot ni les lots suivants, et fournit l’issue de chaque envoi. Avec Promise.all(), le rejet de l’identifiant 3 aurait interrompu l’attente du deuxième lot et masqué les autres résultats, alors même que ces envois auraient bel et bien été effectués.
Vous pouvez aussi vous arrêter ici. L’approfondissement est facultatif.
Les repères Cours Café
Pour aller à la source
Documentation de référence. Vérification éditoriale encore à effectuer.
Gardez une trace de cette idée.
Favoris, notes et progression seront disponibles après connexion du stockage distant.