Promise : ce qui échoue d’abord
Deux sondes, dix cas mesurés sur Node : all s’arrête au premier rejet, allSettled rapporte tout dans l’ordre des entrées, any agrège les erreurs, un race vide ne se règle jamais.
BotBrowser Team
Vous voulez la documentation structurée pour Empreinte ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
BotBrowser prend en charge le rejeu d’un fixture autorisé de même origine sous un profil déclaré et la tenue d’un relevé borné de la promesse qui s’est réglée en premier et de celle qui ne s’est jamais réglée, mais il ne peut pas changer la façon dont un runtime ordonne les règlements, ne peut pas convertir un rejet en résolution et ne peut pas faire aboutir une promesse sans entrée. Cette revue exécute deux sondes sur dix cas mesurés et consigne ce que fait chacun des quatre combinateurs quand une entrée rejette, quand les entrées se terminent dans un ordre différent de celui fourni, quand toutes les entrées rejettent, quand la liste d’entrées est vide et quand un rejet n’est jamais traité.
Résumé
Promise.allrejette avec le premier rejet et cesse d’attendre ; la sonde affichefirst; delivered_so_far=0.- Les autres entrées continuent après ce rejet, donc la ligne suivante montre
delivered=slow,later. Promise.allSettledattend chaque entrée et garde l’ordre des entrées, donc une résolution lente est rapportée en premier et une rapide en dernier.Promise.allgarde aussi l’ordre des entrées, donca,b,carrive dans cet ordre même siba fini en premier.Promise.anyrésout à la première résolution ; quand toutes les entrées rejettent, il lève unAggregateErrordont le tableauerrorsgarde l’ordre des entrées.Promise.raceprend le premier règlement, donc un rejet rapide bat une résolution plus lente.- Une entrée vide est le point où les quatre divergent :
alletallSettledaboutissent,anyrejette avecAggregateErroretracene se règle jamais. - Un rejet que personne ne traite est signalé au crochet de rejet non traité du runtime, que la sonde observe comme
orphan.
Sommaire
- Ce que chaque combinateur promet à la fin
- Pourquoi Promise.all s’arrête au premier rejet
- Comment allSettled rapporte chaque issue dans l’ordre des entrées
- Ce qui distingue any de race
- Ce qu’une entrée vide fait à chaque combinateur
- Comment remonte un rejet que personne ne traite
- Ce qu’un combinateur réglé laisse tourner
- Consigner une lecture de combinateur
Ce que chaque combinateur promet à la fin
Les quatre combinateurs prennent la même entrée, un itérable de valeurs et de promesses, et ne diffèrent que par le règlement qu’ils livrent à l’appelant. Cette seule différence décide de chaque mode de défaillance ci-dessous, donc il vaut la peine d’énoncer les quatre contrats au même endroit avant de lire la moindre sortie.
Promise.all aboutit quand chaque entrée aboutit, avec un tableau dans l’ordre des entrées, et rejette dès qu’une entrée rejette. Promise.allSettled aboutit quand chaque entrée se règle, avec un enregistrement par entrée dans l’ordre des entrées, et ne rejette jamais parce qu’une entrée a rejeté. Promise.any aboutit à la première résolution et ne rejette qu’après le rejet de toutes les entrées, avec un AggregateError. Promise.race se règle avec la première entrée qui se règle, résolution ou rejet, et n’a aucun avis sur les autres.
Deux conséquences en découlent directement. D’abord, seuls all et race peuvent rejeter à cause d’une seule entrée ; allSettled ne peut pas rejeter pour cette raison du tout, et any ne rejette que quand la dernière entrée rejette. Ensuite, trois des quatre préservent l’ordre des entrées dans leur résultat, tandis que race renvoie une seule valeur sans tableau à ordonner, ce qui explique qu’un appelant qui doit savoir quelle entrée a gagné doive attacher l’étiquette à l’entrée elle-même plutôt qu’au résultat.
La sonde pilote chaque contrat avec les mêmes trois entrées, une résolution lente, un rejet et une résolution rapide, et étiquette l’issue. Rien dans la sonde ne dépend d’un délai particulier au-delà de l’ordre relatif des trois minuteurs, donc la forme peut être reproduite sur tout runtime qui implémente les quatre méthodes.
node=v26.9.0
all-rejects-early -> first; delivered_so_far=0
all-later-resolution-still-runs -> delivered=slow,later
allSettled-order -> slow,boom,fast
all-input-order -> a,b,c
any-first-fulfilled -> fast
any-all-reject -> AggregateError; errors=e1,e2
race-first-settlement-wins -> fast-reject
empty-input -> all=fulfilled; allSettled=fulfilled:0; any=rejected:AggregateError; race=still-pending
unhandled-rejection-observed -> orphan
non-promise-input -> [1,"two",{"three":3}]
La dernière ligne est la plus discrète. Promise.all accepte des valeurs simples, donc une liste qui mélange nombres, chaînes et objets se résout avec ces valeurs inchangées, et aucune entrée n’a besoin d’être une promesse pour que le combinateur fonctionne. Un appelant qui enveloppe chaque valeur dans Promise.resolve() avant de la passer fait un travail que le combinateur fait déjà.
Pourquoi Promise.all s’arrête au premier rejet
Promise.all se règle au moment où sa première entrée rejette, et la valeur qu’il livre est la raison de ce rejet, pas un tableau. La sonde l’attrape et rapporte à la fois le message et le nombre d’autres entrées qui avaient abouti à ce moment :
all-rejects-early -> first; delivered_so_far=0
Le compte est zéro parce que le rejet est arrivé avant la résolution à dix millisecondes. La moitié importante de cette ligne est ce qu’elle ne dit pas : le combinateur a abandonné, et les entrées non. Chaque entrée déjà en cours continue jusqu’à sa propre fin, ce que la sonde vérifie en attendant puis en affichant ce qui est arrivé ensuite :
all-later-resolution-still-runs -> delivered=slow,later
Les deux entrées restantes ont fini, l’une d’elles longtemps après le rejet qui a mis fin à l’attente. C’est le mode de défaillance qui surprend le plus souvent les appelants. Un Promise.all rejeté n’annule pas ses entrées, car une promesse n’a pas d’opération d’annulation dans le langage ; le combinateur cesse simplement de les observer. Un travail que l’appelant voulait arrêter doit être arrêté par le code qui le possède, généralement au moyen d’un AbortController passé à chaque opération, et le combinateur n’y prend aucune part.
Le même comportement fixe l’attente pour le nettoyage. Un bloc finally sur l’appel Promise.all s’exécute quand le combinateur se règle, donc tôt dans le cas de rejet, si bien que le nettoyage écrit là s’exécute pendant que les autres entrées sont encore en cours. Un appelant qui ferme une ressource partagée dans ce bloc peut la fermer sous un travail qui l’utilise encore. L’ordre sûr est d’arrêter d’abord les entrées, d’attendre qu’elles accusent réception de l’arrêt, puis seulement de libérer la ressource.
Promise.all préserve bien l’ordre des entrées quand il aboutit, et il le fait même si les entrées finissent dans un ordre différent. La sonde fournit trois résolutions qui finissent à vingt-cinq, cinq et quinze millisecondes et relit le tableau :
all-input-order -> a,b,c
L’entrée la plus rapide était b, et le tableau commence toujours par a. Un appelant qui associe résultats et requêtes par index est donc en sécurité, et un appelant qui veut l’ordre d’achèvement doit l’enregistrer dans chaque entrée plutôt que de l’inférer du résultat.
Comment allSettled rapporte chaque issue dans l’ordre des entrées
Une entrée rejetée ne fait jamais échouer Promise.allSettled. La méthode attend le règlement de chacune, puis renvoie un enregistrement par entrée, chacun avec un status de fulfilled ou rejected et soit une value, soit une reason. La sonde passe les mêmes trois formes et affiche les messages dans l’ordre où ils reviennent :
allSettled-order -> slow,boom,fast
La résolution lente est en premier parce qu’elle était la première entrée, le rejet est en deuxième parce qu’il était la deuxième entrée, et la résolution rapide est en dernier même si elle a fini avant les trois autres. C’est la propriété qui fait d’allSettled le bon choix pour un lot dont les échecs ne doivent pas cacher les succès : l’appelant obtient un rapport complet, et chaque enregistrement reste attaché à l’entrée dont il vient.
Le coût est le temps. allSettled ne peut pas se régler avant l’entrée la plus lente, donc un lot avec une entrée qui ne se règle jamais ne produit aucun rapport du tout. Les entrées de la sonde finissent toutes, ce qui cache ce risque ; un appelant qui a besoin d’une borne doit opposer l’appel allSettled à un minuteur et traiter un délai dépassé comme un rapport partiel, ce qui signifie que l’appelant doit aussi décider quoi faire de l’entrée encore en cours. Les enregistrements eux-mêmes sont des objets simples, donc structuredClone face à JSON explique lesquels survivent à une copie dans un enregistrement de journal et lequel est écarté en silence.
Il y a une deuxième différence qui mérite d’être nommée. allSettled rapporte un rejet comme donnée, donc une entrée rejetée ne devient pas une exception sauf si l’appelant la lève. Un appelant qui consigne le rapport et continue a traité chaque échec par construction, et un appelant qui veut retrouver l’ancien comportement d’all doit parcourir le tableau et lever le premier rejet qu’il trouve. Les deux combinateurs sont le même calcul avec deux politiques sur ce qu’il faut faire d’un échec, et la politique vit chez l’appelant.
Ce qui distingue any de race
Promise.any et Promise.race prennent tous deux plusieurs entrées et renvoient tous deux une issue, et ils diffèrent par le règlement qu’ils acceptent. any ignore les rejets jusqu’au rejet de toutes les entrées, donc il résout à la première résolution même si un rejet arrive en premier. race accepte le premier règlement, quel qu’il soit, donc un rejet qui arrive en premier gagne. La sonde montre les deux :
any-first-fulfilled -> fast
race-first-settlement-wins -> fast-reject
Le cas any avait une résolution lente et une résolution rapide, et il a résolu avec la rapide, ce que le nom suggère. Le cas race avait une résolution lente et un rejet rapide, et il a rejeté. Un appelant qui recourt à race pour implémenter un délai dépassé obtient donc un rejet quand le minuteur se déclenche, ce qui est généralement ce qu’il veut, et un appelant qui recourt à any pour implémenter « le miroir qui répond en premier » obtient une résolution, ce qui est aussi généralement ce qu’il veut. Les échanger change la sémantique de défaillance de tout l’appel.
Quand chaque entrée d’any rejette, le combinateur rejette avec un AggregateError dont le tableau errors contient les raisons dans l’ordre des entrées :
any-all-reject -> AggregateError; errors=e1,e2
Ce tableau est la raison pour laquelle any vaut mieux qu’une boucle manuelle. Une boucle manuelle qui garde le dernier rejet perd les raisons antérieures, tandis qu’AggregateError les garde toutes et donne à l’appelant un ordre stable pour rapporter. Un appelant qui consigne seulement error.message voit un message générique et perd le détail ; le champ utile est error.errors.
race n’a pas d’agrégat de ce genre, parce qu’il se règle sur une entrée et n’apprend jamais rien des autres. Un race qui a rejeté ne dit rien à l’appelant des autres entrées, et un appelant qui doit savoir si elles ont aussi échoué doit les inspecter séparément. Cette asymétrie n’est pas un défaut de l’une ou l’autre méthode ; c’est la différence entre une méthode qui attend une catégorie de règlement et une qui prend le premier, quel qu’il soit.
Ce qu’une entrée vide fait à chaque combinateur
Un itérable vide est la plus petite entrée que les quatre méthodes acceptent, et c’est le cas où leurs contrats cessent de se ressembler :
empty-input -> all=fulfilled; allSettled=fulfilled:0; any=rejected:AggregateError; race=still-pending
Promise.all([]) aboutit immédiatement avec un tableau vide, parce que la condition « chaque entrée a abouti » est déjà vraie quand il n’y a aucune entrée. Promise.allSettled([]) fait de même et renvoie un tableau vide, donc un appelant qui additionne un rapport sur un lot vide obtient zéro plutôt qu’une erreur. Promise.any([]) rejette avec un AggregateError dont le tableau errors est vide, parce que la condition « au moins une entrée a abouti » ne peut jamais devenir vraie. Promise.race([]) renvoie une promesse qui ne se règle jamais, parce qu’aucune entrée ne peut la régler.
Le cas race est celui qui peut bloquer un appelant pour de bon. La sonde ne peut pas attendre cette promesse directement, donc elle l’oppose à un minuteur et affiche still-pending quand le minuteur gagne, ce qui est la seule façon d’observer une promesse sans règlement. Un appelant qui construit sa liste d’entrées à partir d’un filtre, et dont le filtre peut ne rien renvoyer, a un chemin de code qui ne résout jamais. Le correctif est de vérifier la longueur avant d’appeler race, ou d’inclure toujours au moins une entrée dont le règlement est garanti, comme un minuteur.
Les quatre issues découlent des contrats de la première section une fois le cas vide lu comme une limite. all a besoin que chaque entrée aboutisse et n’a rien à attendre, allSettled a besoin que chaque entrée se règle et n’a rien à attendre, any a besoin d’une résolution et ne peut jamais en obtenir une, et race a besoin d’un règlement et ne peut jamais en obtenir un. Rien dans le cas vide n’est traité à part dans la spécification ; il découle des définitions, ce qui explique qu’un appelant qui raisonne à partir des définitions prédise correctement les quatre.
Comment remonte un rejet que personne ne traite
Un rejet auquel aucun gestionnaire n’est attaché ne disparaît pas. Le runtime le signale à ce qui écoute les rejets non traités, et la sonde écoute exactement un tour pour l’observer :
unhandled-rejection-observed -> orphan
La sonde attache un écouteur, crée une promesse rejetée sans catch, attend trente millisecondes et retire l’écouteur. Le message arrive, ce qui signifie que le rejet était encore non traité quand le tour s’est terminé. Dans un navigateur, le même événement est unhandledrejection sur la fenêtre, et dans Node c’est l’événement de processus que la sonde utilise, donc la forme de l’observation est la même même si le nom de l’API diffère.
Un combinateur ne crée pas cette situation, et la sonde le vérifie directement. Une seconde sonde règle un Promise.race sur une résolution puis laisse une entrée plus lente rejeter ensuite ; séparément, elle laisse un Promise.all rejeter tôt pendant qu’une autre entrée rejette plus tard. Aucun de ces rejets ultérieurs n’atteint l’écouteur de rejets non traités, alors qu’un rejet nu sans gestionnaire l’atteint :
after-race-fail=false
after-all-fail=false
control-fail=true
La raison est que chaque combinateur attache son propre gestionnaire à chaque entrée au moment où elle est passée, donc une entrée qui rejette après le règlement du combinateur reste observée, simplement écartée. C’est une vraie différence avec l’attente naïve : abandonner un lot ne déclenche pas de rejets non traités, et un environnement qui reste silencieux après un lot en échec dit la vérité.
Ce que l’appelant doit encore faire, c’est conserver le résultat. Le combinateur écarte le règlement ultérieur, donc un rejet qui arrive après coup est perdu si l’entrée ne l’enregistre pas elle-même. Un catch sur chaque entrée qui consigne l’échec et renvoie une valeur sentinelle conserve cet enregistrement et laisse intacte la sémantique du combinateur, et il coûte une ligne par entrée. Un appelant qui s’en passe obtient un résultat propre et aucune trace de l’échec abandonné, ce qui est plus silencieux et plus difficile à déboguer qu’un rejet non traité.
Ce qu’un combinateur réglé laisse tourner
La deuxième ligne de la sonde est la preuve d’une affirmation facile à énoncer et difficile à garder en tête pendant une revue de code : un combinateur réglé a cessé de regarder, pas arrêté le travail. Trois entrées ont été lancées, une a rejeté aussitôt, et les deux autres ont quand même atteint leur propre fin. La promesse du combinateur était réglée pendant tout ce temps.
Cet écart a un coût dans un lot qui partage une ressource. Dix requêtes qui partagent un pool de connexions, un descripteur de fichier ou un budget de limitation de débit continueront toutes à le consommer après que le combinateur a déjà rejeté, parce que chacune est encore dans sa propre opération. Le budget que l’appelant avait prévu pour un lot est dépensé par un lot que l’appelant a déjà abandonné, et le lot suivant commence avec moins que prévu.
Le moyen de combler l’écart est de donner à chaque entrée un moyen de s’arrêter. Un AbortController par lot, dont le signal est passé à chaque opération, convertit le règlement précoce du combinateur en arrêt explicite :
const controller = new AbortController();
try {
await Promise.all(tasks.map(task => task.run(controller.signal)));
} catch (error) {
controller.abort();
throw error;
}
L’appel abort a lieu au même tour que le rejet, donc les entrées restantes l’apprennent à leur prochaine vérification plutôt qu’à leur fin naturelle. C’est la différence entre un lot qui s’arrête et un lot dont on cesse simplement d’attendre le résultat, et c’est la seule partie du motif que le combinateur ne fait pas pour l’appelant.
Le même raisonnement s’applique à race. Un race utilisé comme délai dépassé se règle sur le minuteur, et le vrai travail continue sans que personne ne l’attende, donc un appelant qui interrompt le contrôleur quand le minuteur gagne réussit les deux moitiés : l’échéance est appliquée et le travail abandonné est arrêté. Un appelant qui se contente de faire la course et de continuer a appliqué l’échéance et laissé le travail derrière. Le tour où ce minuteur se déclenche est lui-même une question d’ordre, que microtâches et minuteurs couvre.
Consigner une lecture de combinateur
Une lecture de combinateur soutient une affirmation étroite : ce runtime, ces entrées, cet ordre d’arrivée ont produit ces règlements. Cinq champs la gardent vérifiable :
- Le runtime, parce que l’événement de rejet non traité et la forme d’
AggregateErrorsont tous deux fournis par le runtime. - Le combinateur et le nombre d’entrées, parce que le cas vide et le cas à une seule entrée se comportent autrement que le cas général.
- L’ordre d’arrivée des entrées, parce qu’
alletallSettledrapportent l’ordre des entrées et qu’un relevé sans lui ne peut pas expliquer pourquoi une entrée lente est arrivée en premier. - Quelles entrées tournaient encore quand le combinateur s’est réglé, parce que c’est la différence entre une attente arrêtée et un travail arrêté.
- Si un rejet a été traité, parce qu’un rejet non traité apparaît dans un autre canal que le résultat.
Sources publiques
- MDN : Promise.all()
- MDN : Promise.allSettled()
- MDN : Promise.any()
- MDN : Promise.race()
- MDN : AggregateError
- Node.js : événement unhandledRejection
- BotBrowser : référence de cohérence de l’automatisation
Conclusion pratique
Choisissez le combinateur selon la politique de défaillance dont l’appelant a besoin, pas selon la forme de l’entrée : all quand un échec doit mettre fin à l’attente, allSettled quand chaque issue doit être rapportée, any quand un succès suffit, et race quand le premier règlement, quel qu’il soit, est la réponse. Rappelez-vous qu’un combinateur réglé n’arrête pas ses entrées : arrêtez-les vous-même et traitez chaque rejet que vous êtes sur le point de cesser d’observer. Protégez la liste vide avant d’appeler race, parce qu’une course sur aucune entrée ne se règle jamais. Pour l’ordre dans lequel ces règlements arrivent, lisez microtâches et minuteurs ; pour ce qui survit à une copie des données que porte une promesse réglée, lisez structuredClone face à JSON.
Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?
Articles Connexes
Microtâches et minuteries : l’ordre réel du code
PlateformeContrôles DRM : système de clés, robustesse, sortie
EmpreinteUnicode : quatre identités de texte lues par une page
EmpreinteOrdre des clés : les entiers trient, les chaînes restent
EmpreintestructuredClone face à JSON : copies profondes
EmpreinteICU : alias de fuseau et la TZ que vous rapportez
Faites passer BotBrowser de la recherche à la production
Utilisez ces guides pour comprendre le modèle, puis passez à la validation multi-plateforme, aux contextes isolés et au déploiement navigateur prêt pour l'échelle.