La syntaxe async/await de JavaScript était censée nous sauver de l'enfer des callbacks (callback hell). Au lieu de cela, elle a introduit un problème plus discret et plus insidieux : un code qui semble correct mais qui se comporte de manière imprévisible. On voit await dans le corps de la fonction et on suppose que tout s'arrête poliment, ligne par ligne. Bien souvent, ce n'est pas le cas. Les boucles s'emballent. Des lots entiers s'effondrent à cause d'une seule requête échouée. Les fichiers d'entrée voient apparaître de vilains wrappers async sans raison. Si vous avez rencontré l'un de ces problèmes, ces trois modèles (patterns) permettront de les résoudre.
Arrêtez d'utiliser await à l'intérieur de forEach
Voici une erreur courante qui semble inoffensive à première vue :
const urls = ['/api/user', '/api/posts', '/api/comments'];
urls.forEach(async (url) => {
const res = await fetch(url);
const data = await res.json();
console.log(data);
});
console.log('All done!');
Exécutez ceci, et 'All done!' s'affiche avant même qu'une seule réponse ne revienne. Pourquoi ? forEach exécute le callback pour chaque élément immédiatement. Il n'attend pas la promesse à l'intérieur de chaque itération. Le mot-clé async transforme chaque callback en une promesse que forEach ignore aussitôt. Votre boucle se termine en quelques microsecondes ; les requêtes réseau partent de leur côté. Si vous avez besoin que les erreurs soient gérées dans l'ordre, ou si vous devez garantir qu'une requête se termine avant que la suivante ne commence, ce modèle rompt silencieusement ces deux garanties.
Remplacez-le par une boucle for...of :
const urls = ['/api/user', '/api/posts', '/api/comments'];
for (const url of urls) {
const res = await fetch(url);
const data = await res.json();
console.log(data);
}
console.log('All done!');
Désormais, la boucle s'arrête réellement à chaque await. La deuxième requête attend la première. 'All done!' ne s'affiche qu'une fois que tout est terminé.
Utilisez for...of lorsque l'ordre est important — l'envoi de fichiers un par un pour respecter les limites de débit (rate limits), l'écriture de lignes de base de données dans un ordre spécifique, ou l'enchaînement d'appels API où la requête suivante a besoin des données de la réponse précédente. Si vous souhaitez réellement une exécution parallèle, ne tâtonnez pas avec forEach. Utilisez explicitement Promise.all afin que votre intention soit claire pour le prochain développeur. Mais ne mélangez jamais await et forEach en espérant un comportement synchrone. Cela n'arrivera pas.
Utilisez Promise.allSettled quand l'échec total n'est pas une option
Promise.all est sémantiquement honnête. Donnez-lui un tableau de promesses, et il renvoie un tableau de résultats. Le piège est qu'au moment même où une seule promesse est rejetée, l'ensemble est rejeté immédiatement. Toutes les autres promesses en attente sont laissées à elles-mêmes pour se terminer, mais vous perdez l'accès à leurs résultats. En production, ce comportement de type "tout ou rien" est problématique.
Imaginez que votre application récupère les widgets d'un tableau de bord à partir de quatre services indépendants : l'analyse du trafic, les données de revenus, les retours utilisateurs et l'état du serveur. L'API des revenus subit un bref timeout. Avec Promise.all, l'intégralité de votre tableau de bord renvoie une erreur. Les trois réponses valides disparaissent dans le néant. L'utilisateur voit un spinner, puis un écran d'erreur, simplement parce qu'un quart des données a mal fonctionné.
Promise.allSettled vous offre un contrat plus sain. Il attend que chaque promesse soit terminée, quel que soit son issue. La valeur résolue est un tableau d'objets décrivant chaque résultat :
const requests = [
fetch('/api/traffic'),
fetch('/api/revenue'),
fetch('/api/feedback'),
fetch('/api/health')
];
const results = await Promise.allSettled(requests);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
renderWidget(index, result.value);
} else {
renderError(index, result.reason);
}
});
Aucune réponse n'est jetée. Vous affichez ce que vous pouvez et isolez l'échec. Ce modèle est crucial dès que vous traitez des opérations sans lien entre elles — notifications groupées, envois de webhooks tiers ou importation d'enregistrements à partir de plusieurs flux CSV. Vous aurez toujours besoin d'un suivi d'erreurs centralisé, mais votre application garde le cap.
Une note pratique : allSettled renvoie l'ensemble complet, vous devez donc toujours trier les résultats et décider de ce que signifie un "succès partiel" pour votre fonctionnalité. Ne traitez pas le tableau renvoyé comme un ensemble de données uniformément positives. Vérifiez les champs status avant d'injecter quoi que ce soit dans votre couche d'état (state layer).
Déclarez le top-level await et supprimez le wrapper IIFE
Pendant des années, si vous vouliez attendre quelque chose à la racine d'un fichier, vous l'enveloppiez dans une fonction async invoquée immédiatement (IIFE) :
(async () => {
const config = await loadConfig();
startServer(config);
})();
Cela fonctionne, mais c'est du bruit inutile. Le top-level await, natif dans les modules ES, vous permet de supprimer ce code répétitif (boilerplate) cérémoniel :
const config = await loadConfig();
startServer(config);
Utilisez cela au point d'entrée de votre application ou dans des modules de configuration dédiés où l'initialisation doit être terminée avant que quoi que ce soit d'autre ne s'exécute. Le chargement de fichiers d'environnement, l'établissement d'un pool de connexions à une base de données ou la récupération de feature flags distants sont des cas d'usage naturels. Comme le top-level await bloque l'exécution du graphe de modules — les autres fichiers important celui-ci attendront la résolution de votre promesse — vous obtenez un état garanti. Le reste de votre code peut importer db en sachant que la connexion est déjà établie.
Il y a deux bémols. Premièrement, votre environnement d'exécution ou votre bundler doit prendre en charge les modules ES. Dans Node.js, cela signifie soit utiliser l'extension .mjs, soit définir "type": "module" dans votre package.json. Deuxièmement, comme l'attente au niveau du module retarde chaque importateur, veillez à ce que le travail attendu soit ciblé. Des récupérations séquentielles lourdes en haut d'un fichier utilitaire fréquemment importé ralentiront le démarrage à froid (cold start) de toute votre application. Réservez le top-level await aux véritables tâches de bootstrap dont les autres modules dépendent réellement.
Ce qui change réellement lorsque vous adoptez ces modèles
La prévisibilité est le premier bénéfice. Lorsque vous lisez une boucle for...of, vous savez exactement quand le bloc sous-jacent se termine. Il n'y a pas de promesses fantômes qui s'exécutent en arrière-plan, pas de callbacks foreach qui se détachent de vos gestionnaires d'erreurs. Votre flux de contrôle correspond à la structure du code à l'écran.
La résilience vient ensuite. Promise.allSettled vous oblige à réfléchir à l'échec partiel plutôt qu'à espérer que chaque système externe reste parfait. Un logiciel de production n'est pas binaire. Certains points de terminaison seront instables. Certaines lectures de fichiers rencontreront des erreurs de permission. Concevoir pour la réalité des échecs éparpillés permet de maintenir votre application opérationnelle sans occulter des données légitimes.
La clarté lie le tout. for...of se lit comme une progression naturelle. allSettled exprime son intention dans son nom. Le top-level await supprime les wrappers IIFE cryptiques afin que vos fichiers d'entrée commencent par la logique métier plutôt que par des acrobaties syntaxiques. Le prochain ingénieur qui touchera au fichier — que ce soit vous dans six mois ou un collègue pressé par les délais — vous remerciera.
Ce qu'il faut retenir
Ne considérez pas async/await comme un correctif global que l'on saupoudre sur du code existant. Auditez vos projets actuels pour détecter ces trois anti-patterns spécifiques. Recherchez les await à l'intérieur des blocs forEach et remplacez-les par des for...of ou un Promise.all intentionnel. Examinez chaque Promise.all qui communique avec des services externes et demandez-vous si un seul échec devrait réellement faire échouer toute l'opération ; si ce n'est pas le cas, passez à Promise.allSettled et gérez les résultats mixtes. Enfin, supprimez les IIFE asynchrones de vos points d'entrée de modules ES et laissez le top-level await gérer directement votre séquence de bootstrap. Ce sont de petits changements mécaniques, mais ensemble, ils transforment des scripts asynchrones fragiles en un code en lequel vous pouvez réellement avoir confiance.
