Async/Await et Gestion des Erreurs dans Node.js

1. Tổng quan

async/await vous permet d’écrire du code asynchrone dans Node.js qui ressemble à du code synchrone, bien plus lisible que les callbacks ou les chaînes de .then(). Mais si vous ne comprenez pas bien le mécanisme sous-jacent, vous risquez facilement de mal gérer les erreurs, ce qui provoque une UnhandledPromiseRejection et fait planter l’application.

Cet article couvre le fonctionnement d’async/await, les patterns de gestion des erreurs corrects, jusqu’aux erreurs courantes lors de la combinaison avec Promise.all, les boucles et les middlewares Express. Après l’avoir lu, vous saurez comment écrire des fonctions async sûres et déboguer plus rapidement les erreurs asynchrones.

2. Async/Await Hoạt Động Như Thế Nào

Une fonction déclarée async retourne toujours une Promise, peu importe ce que vous return à l’intérieur. await ne fait que « mettre en pause » au point d’appel, en cédant le contrôle à la boucle d’événements, sans bloquer le thread.

async function getUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  const user = await response.json();
  return user;
}

getUser(1).then(user => console.log(user.name));

La ligne await fetch(...) attend que la Promise de fetch se résolve avant de continuer. Si fetch est rejetée (par exemple en cas de perte de connexion réseau), l’erreur sera levée directement au point await — et si vous ne la capturez pas, elle rejetera aussi la Promise retournée par getUser.

Attention : Une fonction async retourne toujours une Promise, même si vous ne faites pas de await à l’intérieur. L’appeler en oubliant await ou .then() est la cause la plus courante du bug « le code ne s’exécute pas dans le bon ordre ».

3. Bắt Lỗi Với try/catch

La façon la plus correcte de gérer les erreurs dans une fonction async est d’entourer les points await avec un bloc try/catch.

async function getOrder(orderId) {
  try {
    const order = await orderRepository.findById(orderId);
    if (!order) {
      throw new Error(`Commande ${orderId} non trouvée`);
    }
    return order;
  } catch (err) {
    console.error('Échec de la récupération de la commande :', err.message);
    throw err; // relancer pour que l'appelant soit informé et gère le problème
  }
}
  • try enveloppe toute la logique susceptible de lever une exception ou de rejeter une Promise.
  • catch (err) capture à la fois les erreurs levées manuellement et les rejets de Promise.
  • throw err à la fin pour ne pas « dévorer » l’erreur — l’appelant reçoit toujours la Promise rejetée.

Attention : Si vous catch une erreur mais ne la throw pas et ne retournez pas de valeur valide, la fonction async se résoudra avec undefined. L’appelant pensera que tout va bien mais les données seront manquantes — c’est un bug très difficile à détecter.

3.1. Custom Error Class

Pour classer les erreurs clairement (erreurs métier, erreurs système…), vous devriez créer votre propre classe d’erreur.

class PaymentError extends Error {
  constructor(message, code) {
    super(message);
    this.name = 'PaymentError';
    this.code = code;
  }
}

async function chargeCard(order) {
  const result = await paymentGateway.charge(order.total);
  if (!result.success) {
    throw new PaymentError('Carte refusée', 'CARD_DECLINED');
  }
  return result;
}

Quand vous capturez l’erreur, vous pouvez vérifier err instanceof PaymentError pour gérer différemment selon le type d’erreur, au lieu de comparer les chaînes de message qui sont faciles à mal écrire.

4. Lỗi Trong Promise.all Và Promise.allSettled

Promise.all exécute plusieurs Promises en parallèle, mais il suffit que une seule soit rejetée pour que tout soit rejeté, les autres Promises continuent de s’exécuter « en arrière-plan » mais leurs résultats sont ignorés.

async function notifyCustomers(customers) {
  const results = await Promise.all(
    customers.map(c => emailService.send(c.email))
  );
  return results;
}

Si emailService.send du client numéro 2 échoue, la fonction entière est rejetée immédiatement — vous ne savez pas si les clients 1, 3 et 4 ont été traités avec succès.

Quand vous voulez connaître le résultat de toutes les Promises, même en cas d’échec, utilisez Promise.allSettled :

async function notifyCustomersSafely(customers) {
  const results = await Promise.allSettled(
    customers.map(c => emailService.send(c.email))
  );

  const failed = results.filter(r => r.status === 'rejected');
  if (failed.length > 0) {
    console.warn(`${failed.length} email(s) non envoyé(s)`);
  }
  return results;
}

Promise.allSettled ne rejette jamais. Chaque élément dans results a la forme { status: 'fulfilled', value } ou { status: 'rejected', reason }. C’est vous qui décidez comment gérer la suite.

[
  { status: 'fulfilled', value: true },
  { status: 'rejected', reason: Error: SMTP timeout },
  { status: 'fulfilled', value: true }
]

5. Lỗi Thường Gặp Khi Dùng Await Trong Loop

Utiliser await dans une boucle forEach est une erreur très courante — le callback de forEach n’attend pas l’asynchrone.

// MAUVAIS : forEach n'attend pas
async function processOrders(orders) {
  orders.forEach(async order => {
    await orderService.process(order);
  });
  console.log('Terminé'); // s'exécute immédiatement, sans attendre le traitement des commandes
}

forEach appelle le callback async puis ignore la Promise retournée, sans attendre sa résolution. console.log('Terminé') s’exécute avant que toute commande soit traitée — si process lève une erreur, cette erreur n’est capturée nulle part, ce qui provoque une UnhandledPromiseRejection.

Corrigez cela avec for...of si vous avez besoin de traiter séquentiellement :

async function processOrdersSequentially(orders) {
  for (const order of orders) {
    await orderService.process(order);
  }
  console.log('Terminé');
}

Ou utilisez Promise.all avec map si vous voulez traiter en parallèle :

async function processOrdersParallel(orders) {
  await Promise.all(orders.map(order => orderService.process(order)));
  console.log('Terminé');
}

6. Xử Lý Lỗi Trong Express Middleware

Express n’attrape pas par défaut les erreurs des fonctions async dans les route handlers — si vous levez une exception à l’intérieur, la requête se bloquera ou le processus crashera sur les anciennes versions de Node (Express 4 ne capture pas automatiquement les rejets de Promise).

// RISQUE : les erreurs async ne sont pas capturées automatiquement par Express
app.get('/orders/:id', async (req, res) => {
  const order = await orderService.getOrder(req.params.id);
  res.json(order);
});

La façon sûre est d’écrire un wrapper qui transmet l’erreur à next() :

function asyncHandler(fn) {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
}

app.get('/orders/:id', asyncHandler(async (req, res) => {
  const order = await orderService.getOrder(req.params.id);
  if (!order) {
    const err = new Error('Commande non trouvée');
    err.statusCode = 404;
    throw err;
  }
  res.json(order);
}));

app.use((err, req, res, next) => {
  res.status(err.statusCode || 500).json({ message: err.message });
});

asyncHandler enveloppe le route handler, et si la Promise retournée est rejetée, .catch(next) transmet l’erreur au middleware de gestion des erreurs d’Express au lieu de la laisser se propager librement.

Attention : Express 5 (en version bêta au moment de la rédaction) capture automatiquement les rejets des async handlers, mais avec Express 4 qui est encore largement utilisé, vous avez toujours besoin de ce wrapper.

7. Timeout Và AbortController

Une erreur facile à négliger : attendre une Promise qui ne se résoudra jamais avec await laisserait la fonction bloquée indéfiniment. Utilisez AbortController pour limiter le temps d’attente.

async function fetchWithTimeout(url, timeoutMs = 3000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    return await response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error(`La requête vers ${url} a expiré après ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(timer);
  }
}

setTimeout appelle controller.abort() après timeoutMs, ce qui fait que fetch rejette avec une erreur nommée AbortError. finally nettoie toujours le minuteur pour éviter une fuite mémoire, qu’il y ait succès ou échec.

8. Viết Test Cho Hàm Async

Utilisez Jest pour tester à la fois les cas de succès et les cas d’erreur.

<!-- package.json (section des dépendances, pas Maven) -->
npm install --save-dev jest
// orderService.js
async function getOrder(orderId, repository) {
  const order = await repository.findById(orderId);
  if (!order) {
    throw new Error(`Commande ${orderId} non trouvée`);
  }
  return order;
}

module.exports = { getOrder };
// orderService.test.js
const { getOrder } = require('./orderService');

describe('getOrder', () => {
  it('retourne la commande quand elle est trouvée', async () => {
    const mockRepo = { findById: jest.fn().mockResolvedValue({ id: 1, total: 100 }) };

    const order = await getOrder(1, mockRepo);

    expect(order).toEqual({ id: 1, total: 100 });
  });

  it('lève une erreur quand la commande n\'est pas trouvée', async () => {
    const mockRepo = { findById: jest.fn().mockResolvedValue(null) };

    await expect(getOrder(99, mockRepo)).rejects.toThrow('Commande 99 non trouvée');
  });
});

mockResolvedValue simule une Promise qui se résout avec une valeur donnée. rejects.toThrow vérifie que la Promise est rejetée avec le message attendu — c’est la façon standard de tester les erreurs asynchrones, au lieu d’envelopper manuellement les appels avec try/catch dans le test.

PASS  ./orderService.test.js
  getOrder
    ✓ retourne la commande quand elle est trouvée (3 ms)
    ✓ lève une erreur quand la commande n'est pas trouvée (2 ms)

9. Những Lỗi Thường Gặp Cần Tránh

  • Oublier await : appeler une fonction async sans await fait que le code s’exécute hors de l’ordre attendu, et les erreurs sont « avalées » silencieusement.
  • Mélanger .then() et await : c’est source de confusion, choisissez un seul style dans une même fonction.
  • Bloc try/catch trop large : envelopper toute la logique, y compris ce qui n’est pas asynchrone, rend difficile à identifier la source de l’erreur.
  • Ne pas relancer dans le catch : l’appelant reçoit undefined au lieu de l’erreur, ce qui crée un bug silencieux dans la couche supérieure.
  • Utiliser forEach avec un callback async : il n’attend pas, utilisez plutôt for...of ou Promise.all.

Attention : Si votre logique n’a aucune opération I/O ou tâche asynchrone, n’étiquetez pas la fonction comme async — cela n’augmente que l’overhead de création de Promise inutile.

10. Kết luận

async/await rend le code asynchrone dans Node.js lisible, mais la gestion correcte des erreurs est ce qui compte vraiment pour une application stable. Enveloppez toujours vos await avec try/catch, relancez les erreurs quand c’est nécessaire, utilisez Promise.allSettled quand vous voulez connaître le résultat de toutes les tâches parallèles, et n’oubliez pas le wrapper pour les route handlers Express. Écrire des tests pour les cas de succès et d’erreur vous aidera à être plus confiant lors du refactoring de la logique asynchrone plus tard.

nodejsasync-awaitjavascriptgestion-erreurspromise