Maitriser async/await en JavaScript : iterateurs asynchrones et Promise.all

Guide complet sur async/await en JavaScript : fonctions asynchrones, iterateurs, Promise.all et pieges a eviter pour un code performant.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 12 min read
Maitriser async/await en JavaScript : iterateurs asynchrones et Promise.all
Table of Contents

Introduction : L’evolution de l’asynchrone en JavaScript

L’histoire de la programmation asynchrone en JavaScript est une evolution fascinante qui reflete la maturation du langage. Comprendre cette evolution est essentiel pour apprecier pleinement la puissance d’async/await.

L’ere des Callbacks (avant ES6)

Au debut, JavaScript ne disposait que des callbacks pour gerer l’asynchrone. Cette approche, bien que fonctionnelle, menait souvent au fameux “callback hell” :

// Le callback hell - difficile a lire et maintenir
getUser(userId, function(err, user) {
  if (err) {
    handleError(err);
    return;
  }
  getOrders(user.id, function(err, orders) {
    if (err) {
      handleError(err);
      return;
    }
    getOrderDetails(orders[0].id, function(err, details) {
      if (err) {
        handleError(err);
        return;
      }
      console.log(details);
    });
  });
});

L’arrivee des Promises (ES6)

ES6 a introduit les Promises, offrant une syntaxe plus elegante avec .then() et .catch() :

// Avec les Promises - meilleure lisibilite
getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => console.log(details))
  .catch(err => handleError(err));

async/await : La syntaxe moderne (ES2017)

Finalement, async/await a revolutionne la facon d’ecrire du code asynchrone en le rendant presque identique au code synchrone :

// Avec async/await - code clair et intuitif
async function getOrderInfo(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const details = await getOrderDetails(orders[0].id);
    console.log(details);
  } catch (err) {
    handleError(err);
  }
}

Dans cet article complet, nous allons explorer en profondeur async/await, des bases jusqu’aux patterns avances, en passant par les pieges courants a eviter.

Syntaxe de base d’async/await

Declaration d’une fonction async

Le mot-cle async transforme n’importe quelle fonction en une fonction qui retourne automatiquement une Promise. Voici les differentes syntaxes possibles :

// Declaration de fonction classique
async function fetchData() {
  return 'donnees';
}

// Expression de fonction
const fetchData = async function() {
  return 'donnees';
};

// Arrow function
const fetchData = async () => {
  return 'donnees';
};

// Methode de classe
class ApiService {
  async getData() {
    return 'donnees';
  }
}

// Methode d'objet
const api = {
  async fetch() {
    return 'donnees';
  }
};

Utilisation de await

Le mot-cle await ne peut etre utilise qu’a l’interieur d’une fonction async. Il met en pause l’execution de la fonction jusqu’a ce que la Promise soit resolue :

async function newUnicorn() {
  // await met en pause jusqu'a ce que fetch soit complete
  const response = await fetch('https://api.example.com/unicorns');

  // Puis attend que response.json() soit complete
  const json = await response.json();

  // Retourne la valeur (encapsulee dans une Promise)
  return json.success;
}

// Appel de la fonction async
newUnicorn()
  .then(success => console.log('Succes:', success))
  .catch(err => console.error('Erreur:', err));

// Ou depuis une autre fonction async
async function main() {
  const success = await newUnicorn();
  console.log('Succes:', success);
}

await au niveau superieur (Top-level await)

Depuis ES2022, vous pouvez utiliser await au niveau superieur dans les modules ES :

// module.mjs
const response = await fetch('https://api.example.com/config');
export const config = await response.json();

// Attention : cela bloque l'import du module

Gestion des erreurs avec try/catch

La gestion des erreurs est cruciale dans le code asynchrone. Avec async/await, vous utilisez les blocs try/catch familiars :

Pattern de base

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    // Verifier si la reponse est OK (status 200-299)
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    // Gerer les erreurs reseau et les erreurs HTTP
    console.error('Erreur lors de la recuperation:', error.message);
    throw error; // Re-lancer pour permettre au code appelant de gerer
  }
}

Gestion granulaire des erreurs

async function complexOperation() {
  let connection;

  try {
    connection = await database.connect();

    try {
      const result = await connection.query('SELECT * FROM users');
      return result;
    } catch (queryError) {
      console.error('Erreur de requete:', queryError);
      throw new Error('La requete a echoue');
    }
  } catch (connectionError) {
    console.error('Erreur de connexion:', connectionError);
    throw new Error('Impossible de se connecter a la base');
  } finally {
    // Toujours nettoyer, meme en cas d'erreur
    if (connection) {
      await connection.close();
    }
  }
}

Pattern avec valeur par defaut

async function fetchWithDefault(url, defaultValue) {
  try {
    const response = await fetch(url);
    return await response.json();
  } catch (error) {
    console.warn(`Utilisation de la valeur par defaut:`, error.message);
    return defaultValue;
  }
}

// Utilisation
const users = await fetchWithDefault('/api/users', []);
const config = await fetchWithDefault('/api/config', { theme: 'light' });

Patterns courants d’execution asynchrone

Execution sequentielle vs parallele

Comprendre la difference entre l’execution sequentielle et parallele est fondamental pour optimiser vos performances.

Execution sequentielle (une a la fois)

// SEQUENTIEL - Les requetes s'executent l'une apres l'autre
async function fetchSequential(urls) {
  const results = [];

  for (const url of urls) {
    const response = await fetch(url); // Attend chaque requete
    const data = await response.json();
    results.push(data);
  }

  return results;
}

// Temps total = temps1 + temps2 + temps3 + ...

Execution parallele (toutes en meme temps)

// PARALLELE - Toutes les requetes demarrent en meme temps
async function fetchParallel(urls) {
  const promises = urls.map(url => fetch(url).then(r => r.json()));
  const results = await Promise.all(promises);
  return results;
}

// Temps total = max(temps1, temps2, temps3, ...)

Le piege de forEach avec await

L’utilisation de forEach avec await est un piege classique :

// NE FAITES PAS CELA - forEach n'attend pas les promises
async function badExample() {
  const data = [1, 2, 3, 4, 5];

  data.forEach(async (e) => {
    const result = await somePromiseFn(e);
    console.log(result);
  });

  console.log('Termine'); // S'affiche AVANT les resultats!
}

// FAITES CELA A LA PLACE - Utilisez for...of pour sequentiel
async function goodSequential() {
  const data = [1, 2, 3, 4, 5];

  for (const e of data) {
    const result = await somePromiseFn(e);
    console.log(result);
  }

  console.log('Termine'); // S'affiche APRES tous les resultats
}

// Ou map + Promise.all pour parallele
async function goodParallel() {
  const data = [1, 2, 3, 4, 5];

  const results = await Promise.all(
    data.map(async (e) => {
      const result = await somePromiseFn(e);
      return result;
    })
  );

  console.log('Resultats:', results);
  console.log('Termine');
}

Promise.all - Execution parallele avec echec rapide

Promise.all execute plusieurs Promises en parallele et echoue des que l’une d’elles echoue :

async function getFriendPosts(user) {
  const friendIds = await db.get("friends", {user}, {id: 1});

  // Toutes les requetes partent en meme temps
  const posts = await Promise.all(
    friendIds.map(id => db.get("posts", {user: id}))
  );

  return posts.flat();
}

// Exemple avec gestion d'erreur
async function fetchMultipleUrls(urls) {
  try {
    const results = await Promise.all(
      urls.map(async url => {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`Failed: ${url}`);
        return response.json();
      })
    );
    return results;
  } catch (error) {
    // Une seule erreur fait echouer tout le Promise.all
    console.error('Au moins une requete a echoue:', error);
    throw error;
  }
}

Promise.race - Premier arrive, premier servi

Promise.race retourne le resultat de la premiere Promise qui se resout (ou rejette) :

// Utile pour implementer un timeout
async function fetchWithTimeout(url, timeoutMs = 5000) {
  const fetchPromise = fetch(url);

  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout!')), timeoutMs);
  });

  return Promise.race([fetchPromise, timeoutPromise]);
}

// Utilisation
try {
  const response = await fetchWithTimeout('/api/slow-endpoint', 3000);
  const data = await response.json();
} catch (error) {
  if (error.message === 'Timeout!') {
    console.error('La requete a pris trop de temps');
  }
}

Promise.allSettled - Recuperer tous les resultats

Promise.allSettled attend que toutes les Promises soient terminees, peu importe leur statut :

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

  // Chaque resultat a un status: 'fulfilled' ou 'rejected'
  const successful = results
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);

  const failed = results
    .filter(r => r.status === 'rejected')
    .map(r => r.reason);

  console.log(`Succes: ${successful.length}, Echecs: ${failed.length}`);

  return { successful, failed };
}

// Exemple pratique: envoi de notifications
async function sendNotifications(users) {
  const results = await Promise.allSettled(
    users.map(user => sendNotification(user))
  );

  const report = {
    sent: results.filter(r => r.status === 'fulfilled').length,
    failed: results.filter(r => r.status === 'rejected').length
  };

  return report;
}

Promise.any - Premier succes

Promise.any retourne la premiere Promise qui reussit (ignore les rejets) :

// Utile pour les fallbacks ou la redondance
async function fetchFromMultipleSources(sources) {
  try {
    // Retourne le premier serveur qui repond avec succes
    const data = await Promise.any(
      sources.map(source => fetch(source).then(r => r.json()))
    );
    return data;
  } catch (error) {
    // AggregateError si TOUTES les promises ont echoue
    console.error('Toutes les sources ont echoue:', error.errors);
    throw error;
  }
}

// Exemple: serveur le plus rapide
async function fetchFromFastestServer() {
  const servers = [
    'https://server1.example.com/api/data',
    'https://server2.example.com/api/data',
    'https://server3.example.com/api/data'
  ];

  return Promise.any(servers.map(url => fetch(url).then(r => r.json())));
}

Iterateurs asynchrones et generateurs

Les iterateurs asynchrones permettent de traiter des flux de donnees asynchrones de maniere elegante et efficace.

for await…of - Iteration asynchrone

La boucle for await...of permet d’iterer sur des sources de donnees asynchrones :

// Exemple avec un generateur asynchrone
async function* delayedRange(max) {
  for (let i = 0; i < max; i++) {
    await delay(1000);
    yield i;
  }
}

// Utilisation avec for await...of
async function processRange() {
  for await (const num of delayedRange(5)) {
    console.log(`Nombre recu: ${num}`);
    // S'affiche toutes les secondes: 0, 1, 2, 3, 4
  }
}

Generateurs asynchrones

Les generateurs asynchrones combinent async et function* pour creer des iterateurs puissants :

// Generateur pour paginer une API
async function* fetchPaginatedData(baseUrl) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(`${baseUrl}?page=${page}`);
    const data = await response.json();

    yield* data.items; // Yield chaque item individuellement

    hasMore = data.hasNextPage;
    page++;
  }
}

// Utilisation - traite les donnees au fur et a mesure
async function processAllData() {
  for await (const item of fetchPaginatedData('/api/items')) {
    await processItem(item);
    console.log(`Traite: ${item.id}`);
  }
}

Lecture de fichiers en streaming

// Lire un fichier ligne par ligne (Node.js)
async function* readLines(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({ input: fileStream });

  for await (const line of rl) {
    yield line;
  }
}

// Utilisation
async function processLargeFile() {
  let lineCount = 0;

  for await (const line of readLines('./large-file.txt')) {
    lineCount++;
    if (line.includes('ERROR')) {
      console.log(`Erreur ligne ${lineCount}: ${line}`);
    }
  }
}

Transformer des streams asynchrones

// Generateur de transformation
async function* filterAndTransform(source, predicate, transformer) {
  for await (const item of source) {
    if (predicate(item)) {
      yield transformer(item);
    }
  }
}

// Pipeline de traitement
async function processDataPipeline() {
  const source = fetchPaginatedData('/api/users');

  const activeUsers = filterAndTransform(
    source,
    user => user.isActive,
    user => ({ id: user.id, name: user.name.toUpperCase() })
  );

  for await (const user of activeUsers) {
    console.log(user);
  }
}

Patterns avances

Retry avec backoff exponentiel

Implementez une logique de retry robuste pour les operations qui peuvent echouer temporairement :

async function retryWithBackoff(
  fn,
  maxRetries = 3,
  baseDelay = 1000,
  maxDelay = 30000
) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      // Ne pas retry si c'est une erreur permanente
      if (error.status === 400 || error.status === 401) {
        throw error;
      }

      // Calcul du delai avec jitter aleatoire
      const delay = Math.min(
        baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
        maxDelay
      );

      console.log(`Tentative ${attempt + 1} echouee. Retry dans ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

// Utilisation
const data = await retryWithBackoff(
  async () => {
    const response = await fetch('/api/unstable-endpoint');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  },
  5,    // maxRetries
  1000, // baseDelay (1s)
  30000 // maxDelay (30s)
);

Timeout avec Promise.race

Pattern reutilisable pour ajouter un timeout a n’importe quelle Promise :

function withTimeout(promise, ms, errorMessage = 'Operation timed out') {
  const timeout = new Promise((_, reject) => {
    const id = setTimeout(() => {
      clearTimeout(id);
      reject(new Error(errorMessage));
    }, ms);
  });

  return Promise.race([promise, timeout]);
}

// Utilisation
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
  const controller = new AbortController();
  const { signal } = controller;

  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { ...options, signal });
    return response;
  } finally {
    clearTimeout(timeout);
  }
}

// Exemple avec AbortController pour annuler proprement
async function fetchData() {
  try {
    const response = await fetchWithTimeout('/api/data', {}, 3000);
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.error('Requete annulee (timeout)');
    }
    throw error;
  }
}

Queue de taches asynchrones

Gerez l’execution controlee de taches avec une limite de concurrence :

class AsyncQueue {
  constructor(concurrency = 3) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }

  async add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.running >= this.concurrency || this.queue.length === 0) {
      return;
    }

    const { task, resolve, reject } = this.queue.shift();
    this.running++;

    try {
      const result = await task();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.running--;
      this.process();
    }
  }
}

// Utilisation
const queue = new AsyncQueue(3); // Max 3 taches en parallele

const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
const results = await Promise.all(
  urls.map(url => queue.add(() => fetch(url).then(r => r.json())))
);

Semaphore pour limiter la concurrence

class Semaphore {
  constructor(max) {
    this.max = max;
    this.count = 0;
    this.waiting = [];
  }

  async acquire() {
    if (this.count < this.max) {
      this.count++;
      return;
    }

    await new Promise(resolve => this.waiting.push(resolve));
    this.count++;
  }

  release() {
    this.count--;
    if (this.waiting.length > 0) {
      const next = this.waiting.shift();
      next();
    }
  }

  async use(fn) {
    await this.acquire();
    try {
      return await fn();
    } finally {
      this.release();
    }
  }
}

// Utilisation - limiter les requetes API
const apiLimiter = new Semaphore(5);

async function fetchWithLimit(url) {
  return apiLimiter.use(async () => {
    const response = await fetch(url);
    return response.json();
  });
}

Bonnes pratiques

1. Toujours gerer les erreurs

Ne laissez jamais une Promise sans gestion d’erreur :

// MAL - Erreur silencieuse
async function bad() {
  fetchData(); // Promise non attendue, erreur perdue
}

// BIEN - Erreur geree
async function good() {
  try {
    await fetchData();
  } catch (error) {
    logger.error('Fetch failed:', error);
    // Gerer l'erreur appropriement
  }
}

2. Eviter les awaits sequentiels inutiles

// LENT - Execution sequentielle
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);

// RAPIDE - Execution parallele
const [user, posts, followers] = await Promise.all([
  getUser(id),
  getPosts(id),
  getFollowers(id)
]);

3. Utiliser Promise.allSettled pour les operations non-critiques

// Si une erreur ne doit pas bloquer les autres
const results = await Promise.allSettled([
  sendEmail(user),
  updateAnalytics(event),
  syncWithCRM(data)
]);

// Verifier les erreurs apres
results.forEach((result, index) => {
  if (result.status === 'rejected') {
    logger.warn(`Operation ${index} failed:`, result.reason);
  }
});

4. Preferer les fonctions nommees pour le debugging

// Les stack traces sont plus claires avec des fonctions nommees
const fetchUsers = async function fetchUsers() {
  return await api.get('/users');
};

// Plutot que des fonctions anonymes
const fetchUsers = async () => await api.get('/users');

5. Eviter async dans les constructeurs

// MAUVAIS - Les constructeurs ne peuvent pas etre async
class BadExample {
  constructor() {
    this.data = await fetchData(); // SyntaxError!
  }
}

// BIEN - Utiliser une methode factory
class GoodExample {
  constructor(data) {
    this.data = data;
  }

  static async create() {
    const data = await fetchData();
    return new GoodExample(data);
  }
}

const instance = await GoodExample.create();

6. Nettoyer les ressources avec finally

async function processWithCleanup() {
  const connection = await database.connect();

  try {
    await connection.query('...');
    await connection.query('...');
  } finally {
    // Toujours nettoyer, meme en cas d'erreur
    await connection.close();
  }
}

Pieges courants a eviter

1. Oublier await

// PIEGE - La fonction retourne avant que le fetch soit termine
async function fetchData() {
  const response = fetch('/api/data'); // Oubli de await!
  return response.json(); // Erreur: response est une Promise, pas une Response
}

// CORRECT
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

2. try/catch autour du mauvais code

// PIEGE - L'erreur n'est pas attrapee
async function bad() {
  try {
    const promise = fetchData();
  } catch (error) {
    console.log('Caught!'); // Ne sera jamais execute
  }
  await promise; // L'erreur est lancee ici, en dehors du try/catch
}

// CORRECT
async function good() {
  try {
    const result = await fetchData();
  } catch (error) {
    console.log('Caught!'); // Sera execute si fetchData echoue
  }
}

3. Creer des deadlocks accidentels

// PIEGE - Deadlock potentiel
async function deadlock() {
  const lockA = await acquireLock('A');
  const lockB = await acquireLock('B'); // Si un autre thread a B et attend A...

  // Code...

  await releaseLock('B');
  await releaseLock('A');
}

// MIEUX - Utiliser un timeout ou un ordre d'acquisition coherent
async function safer() {
  const locks = await Promise.race([
    acquireBothLocks('A', 'B'),
    timeout(5000, 'Lock acquisition timeout')
  ]);

  try {
    // Code...
  } finally {
    await releaseAllLocks(locks);
  }
}

4. Lancer des erreurs non-Error

// PIEGE - Difficile a debugger
async function bad() {
  throw 'Something went wrong'; // String au lieu de Error
  throw { message: 'error' };   // Object sans stack trace
}

// CORRECT - Toujours lancer des objets Error
async function good() {
  throw new Error('Something went wrong');
  throw new CustomError('Specific error', { code: 'ERR_001' });
}

5. Ignorer les Promises non attendues

// PIEGE - L'erreur est silencieuse
async function handler(req, res) {
  saveToDatabase(req.body); // Pas d'await, pas de catch
  res.send('OK');
}

// CORRECT - Gerer toutes les Promises
async function handler(req, res) {
  try {
    await saveToDatabase(req.body);
    res.send('OK');
  } catch (error) {
    res.status(500).send('Error saving data');
  }
}

Conclusion

async/await a revolutionne la programmation asynchrone en JavaScript, rendant le code plus lisible et maintenable. Voici un tableau comparatif des trois approches :

CritereCallbacksPromisesasync/await
LisibiliteFaible (callback hell)Moyenne (chaines)Excellente (synchrone-like)
Gestion erreursManuelle (error-first).catch()try/catch natif
DebuggingDifficileMoyenFacile (stack traces)
CompositionComplexeBonneExcellente
AnnulationNon standardNon standardAbortController
IterationTres complexePossiblefor await...of

Points cles a retenir

  1. async/await est du sucre syntaxique sur les Promises - les deux sont interoperables
  2. Privilegiez l’execution parallele avec Promise.all quand les operations sont independantes
  3. Gerez toujours les erreurs avec try/catch ou .catch()
  4. Evitez forEach avec await - utilisez for...of ou map + Promise.all
  5. Utilisez les generateurs asynchrones pour les streams de donnees
  6. Implementez des patterns de retry pour les operations instables
  7. Limitez la concurrence avec des semaphores ou des queues

En maitrisant ces concepts et patterns, vous serez capable d’ecrire du code asynchrone robuste, performant et maintenable qui repond aux exigences des applications modernes.

Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles