Async JavaScript - 09 - Les combinateurs de Promises

Promise.all, allSettled, race et any : quand utiliser lequel, avec des patterns concrets.

09 - Les combinateurs de Promises

Ce que tu vas apprendre

  • Les quatre combinateurs : all, allSettled, race, any
  • Leurs comportements en cas d'échec (et c'est la que ca diverge)
  • Des patterns concrets : timeout, fetches parallèles, resilience

Prerequisites

Avoir lu l'article 08 - Gestion des erreurs asynchrones.


Tu as trois appels API a faire. Tu pourrais les enchaîner avec trois await successifs. Le problème : si chaque appel prend 200ms, tu attends 600ms. Alors qu'ils sont independants. Les combinateurs de Promises te permettent de les orchestrer en parallèle, avec des stratégies d'échec différentes selon tes besoins.

Promise.all : tout ou rien

Promise.all prend un tableau de Promises et retourne une Promise qui se resolve quand toutes sont resolues. Si une seule echoue, tout echoue.

typescriptconst [users, posts, comments] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchComments(),
]);
// Les trois fetches tournent en parallele
// Si un seul echoue, le catch attrape la premiere erreur

Le comportement "fail fast" signifie que des que la première Promise est rejetee, Promise.all est rejetee aussi. Les autres Promises continuent de tourner (elles ne sont pas annulees), mais leur résultat est ignore.

typescripttry {
  const results = await Promise.all([
    fetch("/api/critical"),    // echoue apres 50ms
    fetch("/api/slow"),        // aurait reussi apres 2s
    fetch("/api/medium"),      // aurait reussi apres 500ms
  ]);
} catch (error) {
  // On recoit l'erreur de /api/critical
  // Les deux autres fetches sont toujours en cours (pas annulees)
}

Utilise Promise.all quand : tu as besoin de tous les résultats et qu'un seul échec rend le tout inutile. Chargement d'une page qui depend de trois API, par exemple.

Promise.allSettled : tout le monde termine

Promise.allSettled attend que toutes les Promises soient terminees, qu'elles reussissent ou echouent. Elle ne rejette jamais.

typescriptconst results = await Promise.allSettled([
  fetch("/api/users"),
  fetch("/api/posts"),
  fetch("/api/comments"),
]);

// results est un tableau de :
// { status: "fulfilled", value: Response }
// { status: "rejected", reason: Error }

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log("OK:", result.value);
  } else {
    console.error("Echec:", result.reason);
  }
}

Utilise Promise.allSettled quand : tu veux faire au mieux avec ce qui reussit. Un dashboard qui affiche plusieurs widgets independants : si l'API des stats plante, tu affiches quand meme les notifications et les messages.

Un helper pour extraire les succes :

typescriptfunction getFulfilled<T>(results: PromiseSettledResult<T>[]): T[] {
  return results
    .filter((r): r is PromiseFulfilledResult<T> => r.status === "fulfilled")
    .map((r) => r.value);
}

const places = getFulfilled(
  await Promise.allSettled(ids.map((id) => fetchPlace(id)))
);

Promise.race : le premier qui répond

Promise.race retourne le résultat de la première Promise qui se settle (resolve ou reject).

typescriptconst fastest = await Promise.race([
  fetch("https://cdn-eu.example.com/data"),
  fetch("https://cdn-us.example.com/data"),
]);

Attention : si la première a se terminer est un rejet, Promise.race est rejetee. C'est "race" au sens litteral, pas "race to success".

Le pattern timeout

Le cas d'usage le plus utile de Promise.race :

typescriptfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error(`Timeout apres ${ms}ms`)), ms);
  });
  return Promise.race([promise, timeout]);
}

// Usage
try {
  const data = await withTimeout(fetch("/api/slow-endpoint"), 3000);
} catch (error) {
  // Soit l'erreur de fetch, soit "Timeout apres 3000ms"
}

Sur paltemps.fr, j'utilise ce pattern pour tous les appels a des API tierces. Trois secondes max, apres c'est un fallback ou un message d'erreur. L'utilisateur ne devrait jamais attendre indefiniment.

Promise.any : le premier qui reussit

Promise.any retourne le résultat de la première Promise qui se resolve. Les rejets sont ignores tant qu'au moins une Promise reussit. Si toutes echouent, tu recois un AggregateError qui contient toutes les erreurs.

typescripttry {
  const response = await Promise.any([
    fetch("https://primary-api.example.com/data"),
    fetch("https://fallback-api.example.com/data"),
    fetch("https://cache-api.example.com/data"),
  ]);
  // On a le resultat du premier serveur qui repond avec succes
} catch (error) {
  // AggregateError : les TROIS ont echoue
  console.error("Toutes les sources ont echoue :", error.errors);
}

La différence avec race est subtile mais critique :

typescript// race : premier qui se SETTLE (resolve ou reject)
// Si le premier a finir est un rejet, race rejette

// any : premier qui se RESOLVE
// Les rejets sont ignores sauf si tout echoue

Utilise Promise.any quand : tu as plusieurs sources de fallback et tu veux la première qui marche.

Comparatif rapide

Combinateur Resolve quand Rejette quand
all Toutes resolues Une seule rejetee
allSettled Toutes terminees Jamais
race La première terminee La première terminee (si rejet)
any La première résolue Toutes rejetees

Pattern : fetches parallèles avec limite

Promise.all avec 500 Promises va lancer 500 requêtes en meme temps. Ton API backend va adorer (non). Pour limiter la concurrence, voir l'article 12 sur la concurrence.

Mais pour un nombre raisonnable de requêtes parallèles :

typescriptasync function fetchAllUsers(ids: string[]) {
  const results = await Promise.allSettled(
    ids.map((id) => fetchUser(id))
  );

  const users = [];
  const errors = [];

  for (const [i, result] of results.entries()) {
    if (result.status === "fulfilled") {
      users.push(result.value);
    } else {
      errors.push({ id: ids[i], error: result.reason });
    }
  }

  if (errors.length > 0) {
    console.warn(`${errors.length}/${ids.length} fetches ont echoue`);
  }

  return users;
}

Pattern : timeout resilient avec any + race

Combiner les combinateurs pour un pattern robuste :

typescriptasync function fetchWithFallbackAndTimeout(url: string) {
  return Promise.any([
    withTimeout(fetch(url), 2000),
    withTimeout(fetch(`${url}?cache=true`), 3000),
  ]);
}

Le premier serveur qui répond en moins de 2s gagne. Si le primaire est lent, le cache a 3s de delai supplementaire. Si les deux timeoutent, AggregateError.

Résumé

  • Promise.all : tout ou rien, fail fast a la première erreur
  • Promise.allSettled : attend tout le monde, ne rejette jamais
  • Promise.race : le premier qui settle gagne (meme si c'est un rejet)
  • Promise.any : le premier qui resolve gagne, AggregateError si tout echoue
  • Le pattern timeout avec race est indispensable pour les appels réseau
  • Combine les combinateurs pour des patterns resilients

Article précédent : 08 - Gestion des erreurs asynchrones Article suivant : 10 - AbortController

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.