Développement Web JavaScript Async/await Promise

JavaScript asynchrone : maîtriser async/await efficacement

Callbacks, Promises, async/await : maîtrisez la programmation asynchrone JavaScript avec des exemples concrets et évitez les erreurs classiques.

Benjamin Schweitzer Benjamin Schweitzer
Mercredi 20 novembre 2024
6 min de lecture
JavaScript asynchrone : maîtriser async/await efficacement

Async/Await : la révolution de l'asynchrone en JavaScript

Avant async/await, gérer l'asynchrone en JavaScript ressemblait à ça :

fetchUser(userId, function(error, user) {
  if (error) return handleError(error);
  fetchPosts(user.id, function(error, posts) {
    if (error) return handleError(error);
    fetchComments(posts[0].id, function(error, comments) {
      if (error) return handleError(error);
      // 5 niveaux d'indentation plus tard...
    });
  });
});

Ce "callback hell" ou "pyramid of doom" rendait le code illisible et difficile à maintenir. Les Promises ont amélioré la situation, puis async/await en ES2017 a tout changé.

Ce guide couvre tout, des bases aux patterns avancés.

Les Promises : la fondation

Avant async/await, il faut comprendre les Promises. Une Promise représente une valeur qui sera disponible dans le futur (ou une erreur).

// Créer une Promise
const maPromesse = new Promise((resolve, reject) => {
  setTimeout(() => {
    const succes = Math.random() > 0.3;
    if (succes) {
      resolve({ id: 1, nom: 'Benjamin' });
    } else {
      reject(new Error('Impossible de charger l'utilisateur'));
    }
  }, 1000);
});

// Consommer une Promise
maPromesse
  .then(user => console.log('Utilisateur :', user))
  .catch(err => console.error('Erreur :', err.message))
  .finally(() => console.log('Requête terminée'));

Les 3 états d'une Promise :

  • Pending : en attente (valeur pas encore disponible)
  • Fulfilled : résolue (.then() est appelé)
  • Rejected : rejetée (.catch() est appelé)

Async/Await : la syntaxe moderne

async/await est du "sucre syntaxique" au-dessus des Promises. Une fonction async retourne toujours une Promise. await suspend l'exécution jusqu'à ce que la Promise se résolve.

// Avec les Promises
function chargerUtilisateur(id) {
  return fetch(`/api/users/${id}`)
    .then(response => {
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    })
    .then(data => data)
    .catch(err => { throw err; });
}

// Avec async/await — identique mais lisible
async function chargerUtilisateur(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const data = await response.json();
  return data;
}

La clé : await ne peut être utilisé que dans une fonction async.

Gestion des erreurs avec try/catch

async function sauvegarderProfil(userId, donnees) {
  try {
    const response = await fetch(`/api/users/${userId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(donnees)
    });

    if (!response.ok) {
      const erreur = await response.json();
      throw new Error(erreur.message || `Erreur ${response.status}`);
    }

    const profil = await response.json();
    console.log('Profil sauvegardé :', profil);
    return profil;

  } catch (err) {
    if (err.name === 'TypeError') {
      // Erreur réseau (pas de connexion)
      console.error('Pas de connexion internet');
    } else {
      console.error('Erreur API :', err.message);
    }
    throw err; // Re-lancer pour que l'appelant puisse gérer
  } finally {
    masquerChargement(); // S'exécute toujours
  }
}

Erreur fréquente : oublier le try/catch et laisser les rejections non gérées.

// ❌ Dangereux : erreur silencieuse
async function charger() {
  const data = await fetch('/api/data').then(r => r.json());
  return data;
}

// ✅ Gérer les erreurs explicitement
async function charger() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) throw new Error(`Erreur ${response.status}`);
    return await response.json();
  } catch (err) {
    console.error('Échec du chargement :', err);
    return null; // ou throw selon le contexte
  }
}

Parallélisme : Promise.all, Promise.allSettled, Promise.race

Promise.all — tout ou rien

Exécute plusieurs Promises en parallèle. Si UNE échoue, tout échoue.

// ❌ Séquentiel — lent (chaque await attend le précédent)
async function chargerDashboard(userId) {
  const user     = await chargerUtilisateur(userId);   // 200ms
  const posts    = await chargerPosts(userId);          // 300ms
  const stats    = await chargerStatistiques(userId);   // 250ms
  // Total : ~750ms
}

// ✅ Parallèle avec Promise.all — beaucoup plus rapide
async function chargerDashboard(userId) {
  const [user, posts, stats] = await Promise.all([
    chargerUtilisateur(userId),   //     chargerPosts(userId),          //  > lancés simultanément
    chargerStatistiques(userId),   // /
  ]);
  // Total : ~300ms (le plus lent)
  return { user, posts, stats };
}

Promise.allSettled — tolérant aux erreurs

Attend que toutes les Promises se terminent, qu'elles réussissent ou échouent :

async function chargerAvecFallback(urls) {
  const resultats = await Promise.allSettled(
    urls.map(url => fetch(url).then(r => r.json()))
  );

  return resultats.map((resultat, index) => {
    if (resultat.status === 'fulfilled') {
      return resultat.value;
    } else {
      console.warn(`URL ${urls[index]} a échoué :`, resultat.reason);
      return null; // Valeur par défaut
    }
  });
}

Promise.race — le plus rapide gagne

// Timeout avec Promise.race
function avecTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout après ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

// Utilisation
try {
  const data = await avecTimeout(fetch('/api/lent'), 3000);
} catch (err) {
  console.error('Requête trop lente :', err.message);
}

Promise.any — le premier succès

// Essayer plusieurs sources, prendre la première disponible
async function chargerDepuisMirrors(urls) {
  try {
    return await Promise.any(
      urls.map(url => fetch(url).then(r => r.json()))
    );
  } catch (err) {
    // AggregateError : toutes ont échoué
    throw new Error('Tous les miroirs sont indisponibles');
  }
}

Patterns avancés

Retry automatique

async function avecRetry(fn, options = {}) {
  const { maxTentatives = 3, delai = 1000, facteur = 2 } = options;
  let derniereErreur;

  for (let tentative = 1; tentative <= maxTentatives; tentative++) {
    try {
      return await fn();
    } catch (err) {
      derniereErreur = err;

      if (tentative < maxTentatives) {
        const attente = delai * Math.pow(facteur, tentative - 1);
        console.log(`Tentative ${tentative} échouée. Nouvel essai dans ${attente}ms...`);
        await new Promise(r => setTimeout(r, attente));
      }
    }
  }

  throw new Error(`Échec après ${maxTentatives} tentatives : ${derniereErreur.message}`);
}

// Utilisation
const data = await avecRetry(
  () => fetch('/api/instable').then(r => r.json()),
  { maxTentatives: 3, delai: 500 }
);

Cache simple pour les requêtes

const cache = new Map();

async function fetchAvecCache(url, ttl = 60000) {
  const maintenant = Date.now();
  const cached = cache.get(url);

  if (cached && (maintenant - cached.timestamp) < ttl) {
    return cached.data;
  }

  const response = await fetch(url);
  const data = await response.json();

  cache.set(url, { data, timestamp: maintenant });
  return data;
}

AbortController — annuler une requête

// Indispensable pour les recherches en temps réel
let controlleur;

async function rechercherProduits(terme) {
  // Annuler la requête précédente
  if (controlleur) controlleur.abort();
  controlleur = new AbortController();

  try {
    const response = await fetch(`/api/produits?q=${terme}`, {
      signal: controlleur.signal
    });
    return await response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('Requête annulée');
      return null;
    }
    throw err;
  }
}

// Dans un input de recherche
input.addEventListener('input', async (e) => {
  const resultats = await rechercherProduits(e.target.value);
  if (resultats) afficherResultats(resultats);
});

Async dans les boucles : les pièges

const ids = [1, 2, 3, 4, 5];

// ❌ forEach ne gère pas les Promises — ne PAS utiliser
ids.forEach(async (id) => {
  const user = await chargerUtilisateur(id); // Non attendu !
  console.log(user);
});
console.log('Terminé'); // S'affiche AVANT les utilisateurs

// ✅ for...of — séquentiel
for (const id of ids) {
  const user = await chargerUtilisateur(id);
  console.log(user);
}

// ✅ Promise.all + map — parallèle
const users = await Promise.all(ids.map(id => chargerUtilisateur(id)));

// ✅ Parallèle avec limite de concurrence
async function avecLimite(items, fn, limite = 3) {
  const resultats = [];
  for (let i = 0; i < items.length; i += limite) {
    const lot = items.slice(i, i + limite);
    const resultatsLot = await Promise.all(lot.map(fn));
    resultats.push(...resultatsLot);
  }
  return resultats;
}

// Charger max 3 utilisateurs en parallèle
const users = await avecLimite(ids, chargerUtilisateur, 3);

Top-level await (modules ES2022)

// Dans un module ES (.mjs ou type="module")
// Plus besoin d'envelopper dans une fonction async !

const config = await fetch('/config.json').then(r => r.json());
const db = await connexionDB(config.dbUrl);

export { config, db };

Maîtriser async/await, c'est comprendre que JavaScript n'attend pas — il délègue. Chaque await dit au moteur : "je reviens quand cette Promise est résolue, vas-y fais autre chose en attendant." C'est ce qui permet à un seul thread de serveur de gérer des milliers de requêtes simultanées sans bloquer.


Pour aller plus loin : MDN async/await

Cet article vous a plu ?

Donnez-lui une note, ça m'aide vraiment !

Partager l'article