aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Développement Web

Node.js async/await : guide de programmation asynchrone

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 :

  • await ne peut s'utiliser qu'à l'intérieur d'une fonction async (ou, dans Node moderne, au niveau supérieur des modules).
  • Une fonction async renvoie toujours une Promise ; quoi que tu return à l'intérieur, l'appelant le reçoit via await ou .then().
  • await n'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

  • await avec forEach dans une boucle. array.forEach(async ...) ne fonctionne pas comme tu l'attends ; forEach n'attend pas les Promises. Utilise un for...of classique pour attendre séquentiellement, ou Promise.all(array.map(...)) pour exécuter en parallèle.
  • Oublier await. Sans await, 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.

Bu kategorideki tüm yazılar →

Devamı için