Programmation asynchrone en JavaScript : des callbacks à async/await
JavaScript est un langage monothread, mais son modèle d'exécution permet de traiter efficacement des milliers d'opérations simultanées sans bloquer le thread principal. Cela est possible grâce à la programmation asynchrone. Dans Node.js, où les opérations d'entrée-sortie (lecture de fichiers, requêtes à la base de données, appels réseau) occupent 90 % du temps, comprendre l'asynchronisme devient une compétence essentielle. Dans ce guide, nous allons analyser les trois étapes évolutives du travail avec l'asynchronisme : les callbacks, les promesses et async/await, ainsi que le fonctionnement interne de l'Event Loop.
1. Callbacks : le fondement de l'asynchronisme
Un callback est une fonction qui est passée en argument à une autre fonction et qui est appelée après la fin d'une opération asynchrone. C'est le modèle le plus ancien et le plus basique en JavaScript. Dans Node.js, de nombreux modules intégrés (fs, http) utilisent des callbacks.
Exemple de lecture d'un fichier avec un callback :
const fs = require('fs');
console.log('Début de la lecture du fichier...');
fs.readFile('example.txt', 'utf8', (err, data) => { if (err) { console.error('Erreur :', err); return; } console.log('Contenu du fichier :', data);});
console.log('Le code continue de s\'exécuter...');La sortie console montrera que "Le code continue de s'exécuter..." apparaîtra avant le contenu du fichier. C'est le comportement non bloquant : le thread principal n'attend pas la fin de la lecture, mais continue son exécution.
Problème des callbacks : Callback Hell
Avec des opérations asynchrones imbriquées, le code se transforme en "pyramide infernale" :
fs.readFile('user.json', (err, userData) => { if (err) throw err; const user = JSON.parse(userData); db.query(`SELECT * FROM posts WHERE user_id=${user.id}`, (err, posts) => { if (err) throw err; fs.writeFile('user_posts.json', JSON.stringify(posts), (err) => { if (err) throw err; console.log('Terminé !'); }); });});Ce code est difficile à lire, à déboguer et à maintenir. La solution : les promesses.
2. Promesses (Promises) : chaînage et gestion des erreurs
Une Promise est un objet représentant le résultat futur d'une opération asynchrone. Elle peut se trouver dans l'un des trois états : pending (en attente), fulfilled (réussie) ou rejected (rejetée avec une erreur).
Création d'une promesse simple :
const readFilePromise = (filename) => { return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }); });};
readFilePromise('example.txt') .then(data => console.log('Données :', data)) .catch(err => console.error('Erreur :', err));Chaînage des promesses (Chaining)
Le principal avantage est la possibilité d'enchaîner des opérations séquentielles sans imbrication :
readFilePromise('user.json') .then(userData => { const user = JSON.parse(userData); return dbQueryPromise(`SELECT * FROM posts WHERE user_id=${user.id}`); }) .then(posts => { return writeFilePromise('user_posts.json', JSON.stringify(posts)); }) .then(() => console.log('Terminé !')) .catch(err => console.error('Erreur dans la chaîne :', err));Exécution parallèle : Promise.all
Lorsque vous devez exécuter plusieurs opérations indépendantes simultanément :
const promise1 = readFilePromise('file1.txt');const promise2 = readFilePromise('file2.txt');const promise3 = readFilePromise('file3.txt');
Promise.all([promise1, promise2, promise3]) .then(results => { console.log('Tous les fichiers sont lus :', results); }) .catch(err => console.error('Une des promesses a échoué :', err));Promise.all se termine avec succès uniquement si toutes les promesses sont résolues. Si au moins une échoue, le catch se déclenche