Les Promises représentent des valeurs asynchrones dans trois états : pending, fulfilled et rejected. async/await est une syntaxe qui rend leur utilisation aussi lisible que du code synchrone, tout en préservant le comportement non bloquant.

Qu'est-ce qu'une Promise ?

Une Promise représente une valeur qui n'est pas encore disponible : c'est un espace réservé pour quelque chose qui arrivera dans le futur.

// Une Promise dans trois états :
// 1. Pending    — en attente du résultat
// 2. Fulfilled  — l'opération a réussi, la valeur est disponible
// 3. Rejected   — l'opération a échoué, l'erreur est disponible

const promise = fetch('/api/users');
// À ce stade, promise est PENDING

// Plus tard, elle sera soit :
// FULFILLED — la réponse est arrivée
// REJECTED  — erreur réseau, erreur serveur

.then() : l'ancienne façon

Avant async/await, les Promises se consommaient avec .then() :

fetch('/api/users')
  .then(response => response.json())
  .then(users => {
    console.log(users);  // Utiliser les données
  })
  .catch(error => {
    console.error('Failed:', error);
  });

Ça s'enchaîne correctement pour les cas simples, mais devient vite illisible avec une logique complexe. Async/await a été conçu pour résoudre ça.

async/await : la façon moderne

async function getUsers() {
  try {
    const response = await fetch('/api/users');
    const users = await response.json();
    return users;
  } catch (error) {
    console.error('Failed:', error);
    throw error;
  }
}

await met en pause l'exécution jusqu'à ce que la Promise se résolve. Le code se lit comme du code synchrone, mais s'exécute de façon asynchrone. Règles : await ne peut s'utiliser qu'à l'intérieur de fonctions async, qui retournent toujours une Promise. Si une fonction async retourne une valeur, elle est enveloppée dans une Promise résolue.

Ce qui se passe quand on oublie await

C'est le bug asynchrone le plus courant dans Playwright :

// BUG : await oublié
test('login test', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'user@test.com');

  page.click('[data-testid="submit"]');  // Pas d'await !

  // Cette assertion s'exécute AVANT que le clic soit terminé
  await expect(page).toHaveURL('/dashboard');  // Peut échouer de façon intermittente
});

Sans await, page.click() démarre mais n'attend pas la fin. La ligne suivante s'exécute pendant que le clic est encore en cours. Dans Playwright, ça produit des tests flaky : parfois le clic se termine à temps, parfois non.

Correction :

await page.click('[data-testid="submit"]');

Exécution parallèle vs séquentielle

Par défaut, await exécute les choses de façon séquentielle :

// Séquentiel : temps total = somme de toutes les attentes
const user = await getUser(1);       // Attendre l'utilisateur
const orders = await getOrders(1);   // Puis attendre les commandes
const profile = await getProfile(1); // Puis attendre le profil

Si les opérations sont indépendantes, exécutez-les en parallèle :

// Parallèle : temps total = la plus longue des attentes
const [user, orders, profile] = await Promise.all([
  getUser(1),
  getOrders(1),
  getProfile(1),
]);

Dans les fixtures Playwright, ce pattern sert à configurer plusieurs choses à la fois :

// Configurer les données de test en parallèle
const [adminToken, testUser] = await Promise.all([
  loginAsAdmin(request),
  createTestUser(request),
]);

Promise.all : exécuter plusieurs opérations en parallèle

// Les trois requêtes partent simultanément
const results = await Promise.all([
  request.get('/api/users'),
  request.get('/api/products'),
  request.get('/api/orders'),
]);

// results est un tableau de réponses
const [usersResp, productsResp, ordersResp] = results;

Important : si UNE SEULE promise dans Promise.all est rejetée, l'ensemble est rejeté.

try {
  const [a, b, c] = await Promise.all([
    fetch('/api/users'),
    fetch('/api/will-fail-with-404'),  // Celle-là échoue
    fetch('/api/products'),
  ]);
  // N'arrive jamais ici si l'une d'elles échoue
} catch (error) {
  // L'une d'elles a échoué
  console.error(error);
}

Promise.allSettled : attendre toutes les opérations, même les échecs

const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/might-fail'),
  fetch('/api/products'),
]);

// Chaque résultat a un statut + une valeur ou une raison
results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log('Success:', result.value);
  } else {
    console.log('Failed:', result.reason);
  }
});

Utilisez allSettled quand vous voulez tous les résultats indépendamment des échecs individuels.

Promise.race : le premier qui arrive gagne

// Pattern de timeout avec Promise.race
const fetchWithTimeout = async (url, timeoutMs) => {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), timeoutMs)
  );

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

try {
  const response = await fetchWithTimeout('/api/slow', 5000);
} catch (error) {
  if (error.message === 'Timeout') {
    console.log('Request took too long');
  }
}

Gestion des erreurs

try/catch avec async/await

async function createUser(data: UserData) {
  try {
    const response = await request.post('/api/users', { data });

    if (!response.ok()) {
      const body = await response.json();
      throw new Error(`API error: ${body.message}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Failed to create user:', error);
    throw error;  // Re-throw pour que l'appelant sache que ça a échoué
  }
}

Dans les tests Playwright

test('handles API error gracefully', async ({ page }) => {
  // Simuler une erreur API
  await page.route('/api/users', route => route.fulfill({ status: 500 }));

  await page.goto('/users');

  await expect(page.getByTestId('error-message')).toBeVisible();
  await expect(page.getByTestId('error-message')).toContainText('Something went wrong');
});

Bugs async courants dans les tests Playwright

await manquant sur les assertions

// BUG
expect(page.getByTestId('button')).toBeVisible();  // Pas d'await — passe toujours !

// CORRECTION
await expect(page.getByTestId('button')).toBeVisible();

await mal utilisé dans les boucles

// BUG : tout s'exécute en parallèle mais les erreurs peuvent ne pas être gérées
const items = ['a', 'b', 'c'];
items.forEach(async (item) => {
  await processItem(item);  // Ces appels tournent en parallèle sans être attendus
});

// CORRECTION : séquentiel
for (const item of items) {
  await processItem(item);
}

// OU parallèle avec une gestion correcte
await Promise.all(items.map(item => processItem(item)));

Conditions de concurrence

// BUG : clic et navigation en compétition
await page.click('[data-testid="submit"]');
// Le submit peut déclencher une navigation ou afficher une erreur de validation
// La ligne suivante peut s'exécuter avant qu'on sache ce qui s'est passé

// CORRECTION : attendre explicitement ce qu'on attend
await Promise.all([
  page.waitForURL('/dashboard'),
  page.click('[data-testid="submit"]'),
]);
// Ou : attendre la réponse
const [response] = await Promise.all([
  page.waitForResponse('/api/auth/login'),
  page.click('[data-testid="submit"]'),
]);

Async/await dans les page objects

Les méthodes de page object doivent être async quand elles attendent quelque chose :

class LoginPage {
  constructor(private page: Page) {}

  // Async car elle navigue
  async navigate(): Promise<void> {
    await this.page.goto('/login');
  }

  // Async car elle effectue des actions
  async login(email: string, password: string): Promise<void> {
    await this.page.fill('[data-testid="email"]', email);
    await this.page.fill('[data-testid="password"]', password);
    await this.page.click('[data-testid="submit"]');
  }

  // Async car elle lit depuis la page
  async getErrorMessage(): Promise<string | null> {
    return this.page.getByTestId('error').textContent();
  }
}

// Utilisation : tout est attendu
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('user@test.com', 'ValidPass1');
const error = await loginPage.getErrorMessage();

Récapitulatif

| Concept | Ce qu'il fait |

|---------|--------------|

| Promise | Espace réservé pour une valeur future |

| async | Marque une fonction comme asynchrone |

| await | Suspend jusqu'à ce que la Promise se résolve |

| Promise.all() | Exécute plusieurs en parallèle, attend toutes |

| Promise.allSettled() | Comme all(), mais continue malgré les échecs |

| Promise.race() | Retourne la première à se résoudre/rejeter |

| try/catch | Gère les erreurs asynchrones |

La règle la plus importante : toujours await les actions et assertions Playwright. Un await oublié est la cause principale des tests Playwright flaky. Si votre test échoue de façon intermittente sans raison évidente, cherchez d'abord les await manquants.

→ See also: Async/Await en Termes Simples (pour les Testeurs Perdus avec les Promesses) | Tests Instables: Pourquoi ils Arrivent et Comment les Éliminer | Gestion des Erreurs JavaScript avec try/catch pour les Ingénieurs QA