Albi, France
Publications

🚀 Maîtriser l'Asynchrone en Node.js : Des Callbacks à l'Async/Await optimisé

30 juin 2025
Partager :
Illustration du code asynchrone optimisé
Node.js est puissant et rapide ⚡, en grande partie grâce à son modèle non bloquant et asynchrone. Mais cette nature asynchrone peut être un véritable casse-tête 🤯 si elle n'est pas gérée correctement. Des "pyramides de l'enfer" aux syntaxes modernes et élégantes, les techniques ont beaucoup évolué. Dans cet article, nous allons explorer ce parcours : des traditionnels callbacks aux Promises, pour enfin arriver à la syntaxe async/await. Pour finir en beauté, nous analyserons une fonction getContent optimisée qui illustre une approche moderne et efficace pour charger des données de manière dynamique. Au commencement de Node.js, tout reposait sur les callbacks. Le principe est simple : au lieu d'attendre qu'une tâche se termine ⏳, on lui passe une fonction (le callback) qu'elle devra "rappeler" une fois son travail fini. Le premier argument de ce callback est, par convention, réservé à une éventuelle erreur. Exemple : Lire un fichier avec un callback 💻
javaScript
import { readFile } from 'fs';

readFile('./mon-fichier.txt', 'utf8', (error, data) => {
    if (error) {
        console.error("Oups, une erreur est survenue :", error);
        return;
    }
    console.log(data);
});
Le problème ? Quand on enchaîne plusieurs opérations asynchrones, on tombe vite dans ce qu'on appelle le "Callback Hell" (l'enfer des callbacks). Schéma du Callback Hell : La Pyramide de l'Enfer
javaScript
operation1(..., (err, data) => {
  // Niveau 1
  operation2(..., (err, data) => {
    // Niveau 2
    operation3(..., (err, data) => {
      // Niveau 3
      // Et ainsi de suite... nested hell
    });
  });
});
Cette structure en pyramide est difficile à lire et à maintenir. Pour résoudre le problème du Callback Hell, les Promises ont été introduites. Une promesse est un objet qui représente l'état d'une opération asynchrone :
  • Pending : L'opération n'est pas encore terminée.
  • Fulfilled (Tenue) ✅ : L'opération a réussi et a retourné une valeur.
  • Rejected (Rejetée) ❌ : L'opération a échoué.
On peut enchaîner les opérations avec .then() pour les succès et gérer toutes les erreurs en un seul endroit avec .catch(). Schéma de l'enchaînement des Promises :
Plain Text
Promesse
  |
  .then( résultat => ... ) // Succès
  |
  .then( résultat => ... ) // Succès
  |
  .catch( erreur => ... )  // Gestion de n'importe quelle erreur dans la chaîne
Exemple : Refactorisation avec les Promises 💻
javaScript
import { promises as fs } from 'fs';

fs.readFile('./mon-fichier.txt', 'utf8')
    .then(data => {
        console.log("Contenu du fichier :", data);
        return fs.writeFile('./copie.txt', data); // On retourne une nouvelle promesse
    })
    .then(() => {
        console.log("Le fichier a été copié avec succès ! 🎉");
    })
    .catch(error => {
        console.error("Une erreur est survenue durant le processus :", error);
    });
C'est déjà beaucoup plus propre ! async/await est une surcouche syntaxique aux Promises qui rend le code asynchrone presque aussi lisible que du code synchrone. C'est la méthode à privilégier aujourd'hui.
  • Le mot-clé async se place devant une fonction pour indiquer qu'elle retourne une promesse.
  • Le mot-clé await se place devant une opération qui retourne une promesse. Il met en pause l'exécution de la fonction async jusqu'à ce que la promesse soit résolue, sans pour autant bloquer le reste de l'application.
Schéma de la logique async/await :
javaScript
async function() {
  try {
    const resultat1 = await operation1(); // Attente...
    const resultat2 = await operation2(); // Attente...
    const resultat3 = await operation3(); // Attente...
    // C'est simple et linéaire !
  } catch (erreur) {
    // Gestion de n'importe quelle erreur
  }
}
Exemple : La même logique avec async/await 💻
javaScript
import { promises as fs } from 'fs';

async function lireEtCopierFichier() {
    try {
        const data = await fs.readFile('./mon-fichier.txt', 'utf8');
        console.log("Contenu du fichier :", data);
        
        await fs.writeFile('./copie.txt', data);
        console.log("Le fichier a été copié avec succès ! 🎉");
    } catch (error) {
        console.error("Une erreur est survenue durant le processus :", error);
    }
}

lireEtCopierFichier();
Le code est linéaire, logique et bien plus facile à suivre. 😎 Maintenant, analysons la fonction que vous avez proposée. Elle est un excellent exemple de l'utilisation moderne de async/await pour charger dynamiquement plusieurs "morceaux" de contenu.
  1. Signature async : La fonction est déclarée async, elle retourne donc implicitement une promesse.
  2. Importations Dynamiques import() : C'est la clé ! import() est une fonction qui retourne une promesse, permettant de charger un module JavaScript de manière asynchrone.
  3. Gestion d'Erreur Robuste : Le bloc try...catch englobe toutes les opérations. Si une seule importation échoue (par exemple, hero.ts n'existe pas 🔥), l'exécution saute directement au bloc catch.
  4. Pattern de Retour [data, error] : La fonction retourne un tableau. Soit [données, null] en cas de succès, soit [null, erreur] en cas d'échec.
Dans le code initial, les await sont séquentiels. C'est sûr, mais pas optimal. Puisque les fichiers sont indépendants, on peut les charger tous en même temps avec Promise.all. Schéma : Séquentiel vs Parallèle
Plain Text
// Séquentiel (await l'un après l'autre)
[Tâche 1]--temps-->[Tâche 2]--temps-->[Tâche 3]--temps-->[ FIN ]
Temps total = T1 + T2 + T3

// Parallèle (Promise.all)
|--[Tâche 1]--temps--|
|--[Tâche 2]----temps----|
|--[Tâche 3]---temps---|
                       |--> [ FIN ]
Temps total = Temps de la tâche la plus longue
Voici la version optimisée : 💨
typeScript
// ... mêmes définitions de types

export default async function getOptimizedContent(pathArray: string[]): Promise<[any, null] | [null, any]> {
    const filePath = `./${pathArray.join('/')}/`;

    try {
        // 1. On lance toutes les importations en parallèle
        const promiseSeo = import(`${filePath}seo.ts`);
        const promiseCards = import(`${filePath}cards.ts`);
        const promiseServices = import(`${filePath}services.ts`);
        const promiseHero = import(`${filePath}hero.ts`);

        // 2. On attend que TOUTES les promesses soient résolues
        const [
            { seo },
            { cards },
            { services },
            { hero }
        ] = await Promise.all([promiseSeo, promiseCards, promiseServices, promiseHero]);
        
        // 3. On assemble le résultat
        const success = { cards, services, hero, seo };
        return [success, null];

    } catch (error) {
        console.error(`Failed to load module at ${filePath}:`, error);
        return [null, error];
    }
}
Avec cette version, le temps de chargement total sera celui du fichier le plus long à charger. C'est une amélioration significative de la performance ! La gestion de l'asynchronisme en Node.js a parcouru un long chemin. Si les callbacks ont posé les bases, les Promises puis async/await ont rendu le code infiniment plus propre, lisible et maintenable. En maîtrisant des outils comme les importations dynamiques et Promise.all, vous pouvez non seulement écrire du code robuste, mais aussi l'optimiser pour offrir des performances maximales.

Foire Aux Questions (FAQ)


Powered by wisp
Commentaires
Sources :
Navigation

Prendre rendez-vous

Je suis disponible pour des consultations, des collaborations ou simplement pour discuter de vos projets. N'hésitez pas à me contacter !