Le flux node async await paraît déroutant au premier abord : les callbacks, les Promises et async/await semblent faire le même travail de façons différentes. En réalité, ce sont trois générations de la même idée. Node.js s'exécute sur un seul thread, et des opérations comme lire un fichier, interroger une base de données ou faire une requête HTTP prennent du temps. Dans ce guide, tu apprendras pourquoi le flux asynchrone est nécessaire, comment on est passé du callback hell aux Promises puis à la syntaxe propre d'async/await, comment gérer les erreurs et comment exécuter des tâches en parallèle.
Pourquoi asynchrone ? Un coup d'œil à l'event loop
Node.js s'exécute sur un seul thread principal et repose sur le principe de ne jamais bloquer ce thread. Lire un fichier sur le disque prend des millisecondes ; si Node attendait simplement tout ce temps, il ne pourrait traiter aucune autre requête entrante entre-temps. À la place, il délègue l'opération au système d'exploitation, passe à d'autres tâches et revient quand le résultat est prêt. Ce mécanisme s'appelle l'event loop.
Une distinction importante : le code asynchrone ne signifie pas des threads s'exécutant en parallèle. Il y a un seul thread ; seules les périodes d'attente sont gérées intelligemment. C'est pourquoi un long calcul CPU (par exemple une énorme boucle) bloque malgré tout l'event loop — async/await n'aide pas là ; il est conçu uniquement pour les opérations qui attendent des I/O.
Première génération : les callbacks et le « callback hell »
Aux débuts de Node, les opérations asynchrones se géraient avec des fonctions de callback. Le résultat d'une opération revient via une fonction que tu passes en argument. La convention de Node est le « error-first callback » : le premier paramètre est l'erreur, le second le résultat.
const fs = require("fs");
fs.readFile("a.txt", "utf8", (err, data) => {
if (err) return console.error(err);
console.log(data);
});
Pour une seule opération, c'est très bien. Mais dès que les opérations dépendent les unes des autres, le code se transforme en pyramide qui dérive vers la droite. C'est ce qu'on appelle le callback hell :
readFile("a.txt", (err, a) => {
readFile("b.txt", (err, b) => {
readFile("c.txt", (err, c) => {
// la vérification d'erreur se répète à chaque niveau
});
});
});
Cette structure est difficile à lire, difficile à gérer côté erreurs et difficile à maintenir. Les Promises sont arrivées précisément pour résoudre ce problème.
Deuxième génération : qu'est-ce qu'une Promise ?
Une Promise est un objet qui représente « une valeur pas encore prête mais qui arrivera dans le futur ». Elle a trois états : pending (en attente), fulfilled (terminée avec succès) et rejected (terminée par une erreur). Tu récupères le résultat avec .then() et l'erreur avec .catch() :
const fs = require("fs/promises");
fs.readFile("a.txt", "utf8")
.then((data) => console.log(data))
.catch((err) => console.error(err));
La vraie force des Promises est le chaînage (chaining). Chaque .then() renvoie une nouvelle Promise, donc au lieu d'une pyramide tu obtiens un flux plat. Malgré cela, la lisibilité baisse dans les chaînes très longues — et c'est là qu'intervient async/await.
Troisième génération : du code d'apparence synchrone avec node async await
async/await est du sucre syntaxique (syntactic sugar) construit au-dessus des Promises ; en dessous, ce sont toujours des Promises. Si tu marques une fonction avec async, tu peux utiliser await à l'intérieur. await « attend » sur cette ligne jusqu'à ce qu'une Promise se résolve, mais il ne bloque pas l'event loop.
const fs = require("fs/promises");
async function lire() {
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");
return a + b;
}
Ce code fait le même travail que la pyramide de callbacks, mais se lit de haut en bas, comme du code synchrone. Trois règles à garder en tête :
awaitne peut s'utiliser qu'à l'intérieur d'une fonctionasync(ou, dans Node moderne, au niveau supérieur des modules).- Une fonction
asyncrenvoie toujours une Promise ; quoi que tureturnà l'intérieur, l'appelant le reçoit viaawaitou.then(). awaitn'accélère pas seulement les I/O ; le travail intensif en CPU bloque toujours le thread principal.
Gestion des erreurs : capture propre avec try/catch
Avec les callbacks, tu devais écrire if (err) à chaque niveau. Avec async/await, tu utilises try/catch comme dans du code ordinaire et synchrone. C'est l'un des plus grands gains pratiques d'async/await :
async function lire() {
try {
const data = await fs.readFile("introuvable.txt", "utf8");
return data;
} catch (err) {
console.error("Lecture échouée :", err.message);
return null;
}
}
Si une expression await est rejetée (rejected), le bloc catch se déclenche. Si tu ne captures pas l'erreur, elle devient une unhandled rejection, ce qui dans les versions modernes de Node peut faire planter le processus. Chaque opération asynchrone a donc besoin d'une stratégie d'erreur : soit un try/catch local, soit un rejet de Promise capté plus haut par l'appelant.
Exécution en parallèle : Promise.all et allSettled
Une erreur fréquente est d'attendre des opérations indépendantes l'une après l'autre sans raison. Le code ci-dessous lit deux fichiers séquentiellement ; le second attend que le premier se termine :
const a = await fs.readFile("a.txt", "utf8"); // celui-ci finit d'abord
const b = await fs.readFile("b.txt", "utf8"); // puis celui-ci démarre
Si les opérations ne dépendent pas l'une de l'autre, les démarrer en même temps et les attendre ensemble est bien plus rapide. Pour cela on utilise Promise.all :
const [a, b] = await Promise.all([
fs.readFile("a.txt", "utf8"),
fs.readFile("b.txt", "utf8"),
]);
Promise.all est rejeté en entier si ne serait-ce qu'une de ses opérations est rejetée. Si tu veux autoriser certaines opérations à échouer tout en voyant chaque résultat, utilise Promise.allSettled ; il renvoie le statut (fulfilled/rejected) de chaque opération séparément.
Erreurs fréquentes
awaitavecforEachdans une boucle.array.forEach(async ...)ne fonctionne pas comme tu l'attends ;forEachn'attend pas les Promises. Utilise unfor...ofclassique pour attendre séquentiellement, ouPromise.all(array.map(...))pour exécuter en parallèle.- Oublier
await. Sansawait, la variable contient un objet Promise non résolu, pas la valeur. - Attente séquentielle inutile. Attendre des opérations indépendantes une par une ralentit tes requêtes sans raison.
Questions fréquentes
Dois-je utiliser async/await ou .then() ?
Les deux utilisent la même base de Promises ; le choix est une question de lisibilité. Dans les flux à nombreuses étapes dépendantes, async/await se lit généralement plus proprement. Pour une seule transformation courte, .then() peut être pratique. Rester cohérent au sein d'une même base de code est le mieux.
await ralentit-il le code ?
await à lui seul ne ralentit pas le code ; il diffère simplement la progression de la fonction jusqu'à ce que la Promise se résolve, et l'event loop continue d'effectuer d'autres tâches entre-temps. La lenteur vient généralement d'attendre des opérations indépendantes de façon séquentielle sans raison — parallélise-les avec Promise.all.
async/await suffit-il pour le travail intensif en CPU ?
Non. async/await ne gère efficacement que les attentes d'I/O. Un calcul lourd (chiffrement, traitement d'image) bloque toujours le thread principal. Pour cela, tu devrais envisager le module worker_threads ou un processus séparé.
Tu veux corriger le flux asynchrone de ton projet Node.js ? Pour nettoyer avec async/await une base de code devenue un callback hell, augmenter les performances grâce au parallélisme, ou construire une API solide de zéro, contacte-moi.