JavaScript

Promesses et async/await en JavaScript : guide pratique

Mark Toledo

Mark Toledo

1 avril 2026

Promesses et async/await en JavaScript : guide pratique

Quand j'ai découvert async/await en 2017, ça a changé ma façon de coder. Pas de manière superficielle — vraiment en profondeur. J'avais des projets Node.js avec des callbacks imbriqués sur cinq niveaux, du code illisible que même moi je ne comprenais plus deux semaines après l'avoir écrit. Async/await a résolu ce problème, mais pour bien l'utiliser, il faut d'abord comprendre ce qui se passe dessous.

Le problème des callbacks

Avant les promesses, tout était géré avec des callbacks. Le principe est simple : tu passes une fonction en argument, elle sera appelée quand l'opération asynchrone se termine.

fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) {
    console.error('Erreur lecture:', err);
    return;
  }
  JSON.parse(data); // Traitement du fichier
});

Le souci arrive quand tu enchaînes plusieurs opérations asynchrones. Tu te retrouves avec ce qu'on appelle le "callback hell" — des fonctions imbriquées dans des fonctions, chacune avec sa propre gestion d'erreur. Sur un projet de migration de base de données, j'ai eu un fichier avec sept niveaux d'imbrication. C'était ingérable.

Les promesses : une abstraction plus propre

Une promesse est un objet qui représente la valeur future d'une opération asynchrone. Elle peut être dans trois états : en attente (pending), résolue (fulfilled) ou rejetée (rejected).

const maPromesse = new Promise((resolve, reject) => {
  setTimeout(() => {
    const succes = true;
    if (succes) {
      resolve('Données récupérées');
    } else {
      reject(new Error('Quelque chose a raté'));
    }
  }, 1000);
});

maPromesse
  .then(resultat => console.log(resultat))
  .catch(erreur => console.error(erreur));

L'avantage des promesses sur les callbacks, c'est le chaînage. Chaque .then() retourne une nouvelle promesse, ce qui permet d'écrire des séquences d'opérations de façon linéaire.

fetch('/api/utilisateur/1')
  .then(response => response.json())
  .then(utilisateur => fetch(`/api/commandes/${utilisateur.id}`))
  .then(response => response.json())
  .then(commandes => console.log(commandes))
  .catch(err => console.error('Erreur:', err));

C'est mieux que les callbacks imbriqués, mais ça reste verbeux. Et la gestion d'erreurs avec .catch() à la fin ne capture que certains types d'erreurs si tu n'y fais pas attention.

Async/await : la lisibilité au service de l'asynchronisme

Async/await est du sucre syntaxique par-dessus les promesses. Une fonction async retourne toujours une promesse, et await interrompt l'exécution de cette fonction jusqu'à ce que la promesse se résolve.

async function recupererCommandes(userId) {
  const response = await fetch(`/api/utilisateur/${userId}`);
  const utilisateur = await response.json();
  
  const cmdResponse = await fetch(`/api/commandes/${utilisateur.id}`);
  const commandes = await cmdResponse.json();
  
  return commandes;
}

Regarde la différence de lisibilité. Le code ressemble à du synchrone, mais il est bien asynchrone. Quelqu'un qui ne connaît pas les promesses peut comprendre ce que fait cette fonction.

La gestion des erreurs avec try/catch

C'est là où beaucoup de développeurs font des erreurs. Avec async/await, on utilise try/catch comme pour du code synchrone.

async function chargerDonnees() {
  try {
    const response = await fetch('/api/donnees');
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return await response.json();
  } catch (erreur) {
    console.error('Échec du chargement:', erreur.message);
    throw erreur; // Rethrow pour que l'appelant puisse aussi gérer
  }
}

Attention : si tu oublies le try/catch, une promesse rejetée dans une fonction async créera une "unhandled promise rejection". En Node.js, selon la version et ta configuration, ça peut planter le processus entier.

Exécution parallèle avec Promise.all

Sur un projet e-commerce, j'avais besoin de charger simultanément le profil utilisateur, son panier et ses produits favoris. J'ai commis l'erreur d'utiliser await séquentiellement — trois requêtes qui s'enchaînaient alors qu'elles pouvaient partir en parallèle.

// ❌ Séquentiel — lent (300ms + 200ms + 150ms = 650ms)
const profil = await chargerProfil(userId);
const panier = await chargerPanier(userId);
const favoris = await chargerFavoris(userId);

// ✅ Parallèle — rapide (max des trois = 300ms)
const [profil, panier, favoris] = await Promise.all([
  chargerProfil(userId),
  chargerPanier(userId),
  chargerFavoris(userId)
]);

Promise.all lance toutes les promesses simultanément et attend que toutes soient résolues. Si l'une échoue, l'ensemble échoue immédiatement.

Promise.allSettled pour les cas où les échecs sont acceptables

Parfois, tu veux continuer même si certaines promesses échouent. Promise.allSettled attend que toutes les promesses se terminent, qu'elles réussissent ou non.

const resultats = await Promise.allSettled([
  chargerDonneesA(),
  chargerDonneesB(),
  chargerDonneesC()
]);

resultats.forEach(resultat => {
  if (resultat.status === 'fulfilled') {
    console.log('Succès:', resultat.value);
  } else {
    console.warn('Échec partiel:', resultat.reason);
  }
});

J'utilise ce pattern pour les tableaux de bord qui agrègent des données de plusieurs sources. Si une source externe est indisponible, le dashboard affiche quand même le reste.

Les pièges courants

L'await dans une boucle forEach : un classique. forEach ne comprend pas les fonctions async.

// ❌ Ne fonctionne pas comme prévu
const ids = [1, 2, 3];
ids.forEach(async (id) => {
  await traiter(id); // Les awaits sont ignorés
});

// ✅ Avec for...of
for (const id of ids) {
  await traiter(id);
}

// ✅ Ou en parallèle avec Promise.all + map
await Promise.all(ids.map(id => traiter(id)));

Oublier d'attendre une promesse : si tu appelles une fonction async sans await, tu reçois une promesse non résolue au lieu de la valeur.

// ❌ bug silencieux
const donnees = chargerDonnees(); // C'est une promesse, pas les données
console.log(donnees); // Promise { <pending> }

// ✅
const donnees = await chargerDonnees();

Créer ses propres fonctions asynchrones utilitaires

Au fil des projets, j'ai constitué une petite bibliothèque de fonctions utilitaires. En voici une que j'utilise souvent pour ajouter un délai avec possibilité d'annulation.

function attendre(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function reessayer(fn, options = {}) {
  const { tentatives = 3, delai = 1000 } = options;
  
  for (let i = 0; i < tentatives; i++) {
    try {
      return await fn();
    } catch (erreur) {
      if (i === tentatives - 1) throw erreur;
      await attendre(delai * (i + 1)); // Back-off exponentiel simple
    }
  }
}

// Utilisation
const donnees = await reessayer(
  () => fetch('/api/donnees').then(r => r.json()),
  { tentatives: 3, delai: 500 }
);

Async/await dans les classes

Les méthodes de classe peuvent aussi être async, ce qui est pratique pour les repositories ou les services.

class UtilisateurService {
  constructor(db) {
    this.db = db;
  }

  async trouverParId(id) {
    const utilisateur = await this.db.query(
      'SELECT * FROM utilisateurs WHERE id = ?',
      [id]
    );
    return utilisateur[0] ?? null;
  }

  async creer(donnees) {
    const resultat = await this.db.query(
      'INSERT INTO utilisateurs SET ?',
      donnees
    );
    return this.trouverParId(resultat.insertId);
  }
}

Ce qu'on retient

Les promesses et async/await ne sont pas deux choses distinctes — l'un est construit sur l'autre. Comprendre les promesses te permet de mieux utiliser async/await, notamment pour les patterns de concurrence comme Promise.all.

Les erreurs les plus fréquentes : l'await séquentiel là où on pourrait faire du parallèle, l'absence de try/catch, et l'utilisation de forEach avec des fonctions async. Maintenant que tu les connais, tu ne les feras plus.

Pour aller plus loin, explore Promise.race, Promise.any, et les AbortController pour annuler des requêtes fetch en cours — des outils indispensables pour des applications robustes.