
Combiner callbacks, promesses et async/await
Apprenez a gerer et combiner les differents mecanismes d'asynchronisme en Node.js : callbacks traditionnels, Promesses (Promises) et la syntaxe async/await.
La cohabitation des modeles asynchrones en Node.js
L'écosystème Node.js a évolué au fil du temps, et avec lui, les manières de gérer les opérations asynchrones. Initialement, le modèle prédominant était celui des callbacks (fonctions de rappel). Puis sont apparues les Promesses (Promises), offrant une meilleure gestion de l'enchaînement et des erreurs. Enfin, la syntaxe `async/await` est venue simplifier l'écriture du code asynchrone basé sur les Promesses, le rendant plus lisible et semblable à du code synchrone.
Dans un projet Node.js réel, il est très fréquent de devoir faire cohabiter ces trois approches. Vous pourriez utiliser une bibliothèque moderne basée sur `async/await`, tout en devant interagir avec une ancienne API Node.js qui utilise des callbacks, ou intégrer une dépendance qui retourne des Promesses. Savoir comment passer d'un modèle à l'autre et les combiner harmonieusement est donc une compétence essentielle pour écrire du code Node.js maintenable et efficace.
Ce chapitre explore comment ces différents mécanismes peuvent interagir et comment convertir ou adapter le code entre eux.
Utiliser des fonctions a base de callbacks avec Promesses / Async/await
C'est le scénario le plus courant : vous travaillez avec du code moderne utilisant `async/await` ou des Promesses, mais vous devez appeler une fonction (souvent une ancienne API Node ou une bibliothèque tierce) qui attend un callback de type `(err, result)`.
Méthode 1 : `util.promisify` (Recommandée pour les API Node standard)
Node.js fournit un utilitaire très pratique, `util.promisify`, spécialement conçu pour convertir une fonction suivant la convention de callback `(err, value) => ...` en une fonction qui retourne une Promesse.
const fs = require('fs');
const util = require('util');
// Convertir fs.readFile (qui utilise un callback) en une version Promesse
const readFilePromise = util.promisify(fs.readFile);
// Utilisation avec .then/.catch
readFilePromise('mon_fichier.txt', 'utf8')
.then(data => {
console.log('Contenu (promisify + then):', data);
})
.catch(err => {
console.error('Erreur lecture (promisify + then):', err);
});
// Utilisation avec async/await (plus lisible)
async function lireFichier() {
try {
const data = await readFilePromise('mon_fichier.txt', 'utf8');
console.log('Contenu (promisify + async/await):', data);
} catch (err) {
console.error('Erreur lecture (promisify + async/await):', err);
}
}
lireFichier();
`util.promisify` fonctionne parfaitement pour la plupart des fonctions des modules core de Node.js (`fs`, `dns`, `child_process`, etc.).
Méthode 2 : Wrapper manuellement avec `new Promise`
Si `util.promisify` ne fonctionne pas (la fonction n'a pas la signature standard `(err, value)` ou a un comportement particulier), vous pouvez toujours l'envelopper manuellement dans un constructeur `Promise`.
// Supposons une fonction legacy avec un callback non standard
function operationLegacy(arg1, callback) { // callback(result)
setTimeout(() => {
if (arg1 === 'ok') {
callback('Succès ' + Date.now());
} else {
// Simule une absence d'erreur mais un résultat "vide" pour le cas échec
callback(null);
}
}, 500);
}
// Fonction qui la "promisifie" manuellement
function operationLegacyPromise(arg1) {
return new Promise((resolve, reject) => {
// Appeler la fonction legacy
operationLegacy(arg1, (result) => {
// Logique pour déterminer si c'est un succès ou un échec
if (result !== null) {
resolve(result); // Succès -> résoudre la promesse
} else {
// On décide de rejeter si le résultat est null
reject(new Error('Opération legacy a échoué ou retourné null'));
}
// Si operationLegacy avait un paramètre d'erreur (err, result):
// if (err) { reject(err); } else { resolve(result); }
});
});
}
// Utilisation
async function executerOperation() {
try {
const resultatOk = await operationLegacyPromise('ok');
console.log('Résultat OK:', resultatOk);
const resultatKo = await operationLegacyPromise('ko'); // Va rejeter
console.log('Résultat KO (ne devrait pas s\'afficher):', resultatKo);
} catch(err) {
console.error('Erreur attrapée:', err.message);
}
}
executerOperation();
Cette méthode vous donne un contrôle total sur la façon dont le résultat du callback est mappé sur la résolution ou le rejet de la Promesse.
Utiliser des Promesses / Async/await avec du code base sur les callbacks
C'est le scénario inverse : vous avez une fonction qui retourne une Promesse (ou une fonction `async`) et vous devez l'intégrer dans un flux qui attend un callback.
Il suffit d'appeler la fonction retournant une Promesse et d'utiliser ses méthodes `.then()` et `.catch()` pour invoquer le callback approprié.
// Fonction retournant une Promesse
function operationPromise(shouldSucceed) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldSucceed) {
resolve('Données de la promesse');
} else {
reject(new Error('Echec de la promesse'));
}
}, 500);
});
}
// Fonction qui attend un callback et utilise operationPromise
function fonctionAvecCallback(param, callback) {
console.log(`fonctionAvecCallback appelée avec: ${param}`);
const doitReussir = (param === 'reussite');
operationPromise(doitReussir)
.then(result => {
// Succès de la promesse: appeler le callback avec (null, resultat)
callback(null, result);
})
.catch(err => {
// Echec de la promesse: appeler le callback avec (erreur)
callback(err);
});
}
// Tester la fonction avec callback
fonctionAvecCallback('reussite', (err, data) => {
if (err) {
console.error('Callback (reussite) a reçu une erreur:', err);
} else {
console.log('Callback (reussite) a reçu les données:', data);
}
});
fonctionAvecCallback('echec', (err, data) => {
if (err) {
console.error('Callback (echec) a reçu une erreur:', err.message);
} else {
console.log('Callback (echec) a reçu les données:', data);
}
});
De même, si vous appelez une fonction `async`, elle retourne une promesse, vous pouvez donc utiliser exactement la même approche `.then().catch()` pour intégrer son résultat dans un callback.
Combiner Promesses et Async/await : la synergie naturelle
`async/await` est essentiellement une manière plus lisible d'écrire du code qui utilise des Promesses. Elles fonctionnent donc ensemble de manière transparente :
- Le mot-clé `await` attend qu'une Promesse soit résolue et retourne sa valeur résolue, ou lance une exception si la Promesse est rejetée.
- Une fonction déclarée avec `async` retourne toujours une Promesse. Cette promesse se résout avec la valeur retournée par la fonction `async`, ou est rejetée si une exception est levée (ou une promesse rejetée est `await`ed sans `try...catch`).
- Les méthodes utilitaires comme `Promise.all()` (attendre plusieurs promesses en parallèle), `Promise.race()` (attendre la première promesse résolue/rejetée), `Promise.allSettled()` (attendre toutes les promesses, qu'elles réussissent ou échouent) s'intègrent parfaitement avec `async/await`.
// Fonction retournant une promesse après un délai
function delai(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
async function traitementParallele() {
console.log('Début du traitement parallèle...');
try {
// Lancer plusieurs opérations (promesses) en parallèle
const [resultat1, resultat2, resultat3] = await Promise.all([
delai(1000, 'Résultat 1'),
delai(500, 'Résultat 2'), // Celle-ci finit plus tôt
delai(1200, 'Résultat 3')
]);
// Le code ici ne s'exécute que lorsque TOUTES les promesses sont résolues
console.log('Tous les résultats reçus:');
console.log('-', resultat1);
console.log('-', resultat2);
console.log('-', resultat3);
} catch (error) {
console.error('Une des opérations parallèles a échoué:', error);
}
}
traitementParallele();
Conclusion : choisir le bon outil et savoir les ponter
Node.js offre une flexibilité remarquable dans la gestion de l'asynchronisme. Pour les nouveaux développements, `async/await` est généralement l'approche préférée car elle améliore considérablement la lisibilité et la maintenabilité du code asynchrone, tout en bénéficiant de la puissance et de la standardisation des Promesses.
Cependant, la réalité du développement implique souvent d'interagir avec du code existant ou des bibliothèques utilisant des modèles plus anciens. Savoir "promisifier" une fonction à callback (avec `util.promisify` ou manuellement) et savoir intégrer le résultat d'une Promesse ou d'une fonction `async` dans un flux basé sur les callbacks sont des compétences essentielles.
En comprenant comment ces trois paradigmes (callbacks, Promesses, `async/await`) fonctionnent et comment ils peuvent interagir, vous serez en mesure de naviguer efficacement dans n'importe quelle base de code Node.js et de choisir la meilleure approche pour chaque situation, tout en assurant une bonne intégration entre les différentes parties de votre application.