La plupart du temps, personne n'écrit new Promise : une fonction async en renvoie déjà une, et les interfaces modernes en produisent d'elles-mêmes.
Reste un cas où il devient indispensable : quand le travail à attendre n'annonce sa fin que par un callback.
Définition
new Promise prend une fonction, appelée exécuteur, qui reçoit deux fonctions de contrôle : resolve pour livrer une valeur, reject pour signaler un échec. L'exécuteur part immédiatement, dès la création de l'objet.
const attendre = (ms) => new Promise((resolve) => {
setTimeout(() => resolve("fini"), ms);
});
attendre(200).then((v) => console.log(v)); // fini après 200 msLa promesse reste en attente tant que ni resolve ni reject n'a été appelé. Une Promise qui n'appelle jamais l'un des deux n'échoue pas : elle ne se règle jamais, et le await placé devant attend indéfiniment.
Emballer une interface à callback
C'est l'usage principal du constructeur. Une fonction à la convention erreur en premier se convertit sans effort : l'erreur va dans reject, le résultat dans resolve.
function lireConfig(chemin, callback) {
if (!chemin) return callback(new Error("Chemin manquant"));
callback(null, { theme: "sombre" });
}
const lireConfigPromise = (chemin) => new Promise((resolve, reject) => {
lireConfig(chemin, (err, valeur) => {
if (err) reject(err);
else resolve(valeur);
});
});
lireConfigPromise("app.json").then((c) => console.log(c.theme)); // sombreUne fois cette enveloppe posée, l'appel rejoint le reste du code : then(), catch, await et Promise.all() fonctionnent dessus comme sur n'importe quelle promesse.
Les deux pièges du constructeur
- Le premier appel gagne. Un
resolvesuivi d'unrejectne provoque aucune erreur : les appels suivants sont ignorés en silence. Unreturndevant chaque appel évite d'y revenir. - L'exécuteur n'est protégé qu'en surface. Un throw écrit directement dans l'exécuteur devient bien un rejet. Mais un
throwdepuis un callback asynchrone, dans unsetTimeout, part hors de la promesse et personne ne l'attrape.
// Devient un rejet : le throw est dans l'exécuteur
new Promise(() => { throw new Error("visible"); })
.catch((err) => console.log("attrapé :", err.message));
// Ne devient rien : le throw part plus tard, hors de l'exécuteur
// new Promise(() => setTimeout(() => { throw new Error("perdu"); }, 0));Promise.resolve(valeur) et Promise.reject(erreur) fabriquent en une expression une promesse déjà réglée. Pratique pour un cache : la fonction rend une valeur déjà connue ou un appel réseau, sans que l'appelant ait à les distinguer.
Questions fréquentes
Peut-on remplacer le constructeur par une fonction async ?
Presque toujours, oui. Si le travail à attendre rend déjà une promesse, l'envelopper dans new Promise ne fait qu'ajouter une couche et une occasion d'oublier un rejet. Le constructeur ne se justifie que face à un callback, un événement ou une interface qui ignore les promesses.
Que se passe-t-il si on passe une promesse à resolve ?
Elle est adoptée : la promesse extérieure prend l'issue de la promesse intérieure au lieu d'un objet imbriqué. C'est aussi ce qui rend le return d'une promesse transparent dans un then, et ce qui interdit d'obtenir une promesse de promesse.
L'exécuteur peut-il être marqué async ?
Techniquement oui, mais c'est un défaut connu. Les erreurs lancées dans un exécuteur async deviennent le rejet de la promesse interne, qui n'est reliée à rien, et disparaissent du diagnostic. Gardez un exécuteur ordinaire et sortez le travail asynchrone.