La promisification en JavaScript
Apprenez la promisification JavaScript : encapsulez des callbacks dans une Promise, créez un helper promisify() générique et découvrez ses limites.
Qu'est-ce que la promisification ?
La promisification consiste à encapsuler une fonction basée sur des callbacks afin qu'elle retourne une Promise plutôt que de prendre un callback en paramètre. On effectue cette opération une seule fois, puis on peut utiliser .then(), .catch(), le chaînage et async/await sur une fonction qui n'était pas conçue pour cela.
Cette page explique comment encapsuler une API à callback de type error-first, comment créer un helper générique promisify() réutilisable, comment gérer des callbacks qui retournent plusieurs résultats, et les cas où la promisification ne fonctionne pas.
Pourquoi promisifier ?
Les APIs JavaScript plus anciennes et la majeure partie de la bibliothèque standard Node.js communiquent leurs résultats via un callback passé en paramètre. Ce style s'imbrique rapidement et disperse la gestion des erreurs :
getUser(id, (err, user) => {
if (err) return handleError(err);
getOrders(user, (err, orders) => {
if (err) return handleError(err);
getTotal(orders, (err, total) => {
if (err) return handleError(err);
console.log(total);
});
});
});Si ces mêmes fonctions retournaient des promises, la logique s'aplatirait en une seule chaîne linéaire (ou quelques lignes await) avec un seul .catch() pour l'ensemble du flux. La promisification fait le pont entre ces deux mondes — consultez Les callbacks et au-delà pour la partie callback.
La convention du callback error-first
Avant d'encapsuler quoi que ce soit, vous devez connaître la forme que vous encapsulez. Les callbacks Node.js suivent la convention error-first (ou « style Node ») : le callback est le dernier argument et est appelé sous la forme callback(error, result).
- En cas d'échec,
errorest un objetErroretresultest undefined. - En cas de succès,
errorestnulletresultcontient la valeur.
La promisification établit cette correspondance directement : une error non null devient reject(error), et un result réussi devient resolve(result).
Encapsuler une API à callback unique
Voici le schéma de base. On encapsule une fonction à style callback dans une nouvelle Promise, en appelant reject pour l'erreur et resolve pour la valeur. L'exemple simule une API error-first avec setTimeout afin de fonctionner partout, y compris dans le navigateur :
L'encapsuleur accepte le même argument id, le transmet et fournit son propre callback qui fait le pont vers resolve/reject. L'appelant ne voit plus jamais de callback.
Utiliser l'encapsuleur avec async/await
Le vrai bénéfice d'une fonction retournant une promise est qu'elle fonctionne avec async/await, transformant le code asynchrone en quelque chose qui se lit de haut en bas :
Un helper générique promisify()
Écrire un encapsuleur à la main pour chaque fonction devient répétitif. Un helper générique prend n'importe quelle fonction error-first et retourne une version qui retourne une promise. L'astuce consiste à collecter tous les arguments originaux avec un paramètre rest, puis à ajouter notre propre callback :
Parce que le helper utilise ...args et transmet this, il fonctionne pour des fonctions avec n'importe quel nombre d'arguments initiaux. Dans Node.js, la bibliothèque standard fournit exactement cela sous la forme de util.promisify, vous n'avez donc rarement besoin d'écrire le vôtre :
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));Gérer les callbacks avec plusieurs arguments
Le helper simple suppose que le callback fournit un seul résultat : callback(err, result). Certaines APIs transmettent plusieurs valeurs, comme callback(err, header, body). Un simple resolve(result) ignorerait silencieusement tout ce qui suit la première valeur.
Une promise ne peut se résoudre qu'avec une seule valeur, il faut donc collecter les arguments supplémentaires dans un array (ou un object) et résoudre avec celui-ci :
util.promisify de Node prend en charge la même idée via un symbole personnalisé (util.promisify.custom), mais pour des fonctions ponctuelles, un array est l'approche la plus simple.
Limitations et pièges
La promisification est mécanique, mais elle a des limites réelles :
- Elle suppose la convention error-first. Si une fonction signale les erreurs d'une autre manière — par exemple un retour boolean, une exception levée, ou un ordre
(result, err)— un helper générique l'interprétera mal. Encapsulez-les à la main. - Elle ne gère qu'une seule complétion. Les promises se résolvent une seule fois. Une fonction qui invoque son callback plusieurs fois (événements, flux,
setInterval, un callback de progression) ne peut pas être promisifiée — seul le premier appel résoudrait la promise ; les appels suivants sont ignorés. Utilisez une API d'événements ou un itérateur asynchrone pour les valeurs répétées. - Vous ne pouvez pas annuler une promise. Si l'API à callback sous-jacente prend en charge l'annulation (comme l'effacement d'un timer), cette capacité est perdue une fois cachée derrière une promise.
- L'encapsuleur change la signature d'appel. Les appelants doivent désormais utiliser
.then/awaitau lieu de passer un callback. Ne promisifiez pas une fonction que certain code appelle encore en style callback sans conserver les deux versions. - Un throw à l'intérieur de l'exécuteur provoque quand même un rejet. Le code qui s'exécute de manière synchrone à l'intérieur de
new Promise((resolve, reject) => { ... })est intercepté et transformé en rejet — mais une erreur levée ultérieurement à l'intérieur d'un callback asynchrone n'est pas interceptée automatiquement, ce qui explique exactement pourquoi vous devez appelerreject(err)explicitement.
Bonnes pratiques
- Promisifiez à la frontière. Convertissez les APIs d'I/O et de timer une seule fois, là où elles entrent dans votre code, et gardez le reste de votre base de code basée sur les promises.
- Préférez les fonctions intégrées. Dans Node.js, utilisez
util.promisify(ou les modulesfs/promises,dns/promises, etc.) avant d'écrire votre propre encapsuleur. - Gérez toujours le rejet. Attachez un
.catch()ou encapsulezawaitdans untry/catch; un rejet non géré peut faire planter un processus Node. - Gardez des noms prévisibles. Une convention courante est de suffixer la version promise avec
Async(readFileAsync) afin que les deux styles puissent coexister.
Sujets connexes
- La Promise JavaScript — l'objet que vous créez lors de la promisification.
- Le chaînage de Promises — enchaînez proprement des appels promisifiés.
- Async/Await — la syntaxe qui fait lire les fonctions promisifiées comme du code synchrone.
- Les callbacks et au-delà — le schéma à partir duquel vous effectuez la conversion.
- L'API Promise — combinez plusieurs appels promisifiés avec
Promise.allet ses variantes.