Front-end angularforall.com

- Async/Await vs Promises : asynchrone JavaScript

Javascript Async Promises Asynchrone Es7
Async/Await vs Promises : asynchrone JavaScript

Maîtrisez les Promises et Async/Await pour gérer le code asynchrone JavaScript : chaînes de promesses, gestion des erreurs, Promise.all et éviter le callback hell.

Les Promises : qu'est-ce que c'est ?

Une Promise en JavaScript représente la finalisation (ou l'échec) d'une opération asynchrone et sa valeur résultante. C'est un objet qui relie un callback asynchrone à du code réactif.

À retenir : Une Promise encapsule une opération asynchrone et permet de chaîner des actions sans callback hell.
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Succès!');
  }, 1000);
});

promise.then(result => {
  console.info(result); // 'Succès!'
});

États d'une Promise : pending, fulfilled, rejected

Une Promise possède 3 états possibles :

  • pending : opération en cours
  • fulfilled : opération réussie (resolve)
  • rejected : opération échouée (reject)
// ✓ Fulfilled
new Promise(resolve => {
  resolve('Valeur');
}).then(value => console.info(value));

// ✗ Rejected
new Promise((resolve, reject) => {
  reject(new Error('Erreur!'));
}).catch(error => console.error(error));

// Pending (ne terminera jamais)
new Promise(() => {
  // Rien ne se passe
});

Methods des Promises : then, catch, finally

fetch('https://api.example.com/data')
  .then(response => response.json()) // Chaîner then
  .then(data => console.info(data))
  .catch(error => console.error('Erreur:', error)) // Gérer l'erreur
  .finally(() => console.info('Terminé')); // Toujours exécuté

Promise.all() : attendre plusieurs Promises

Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json())
])
  .then(([users, posts, comments]) => {
    console.info(users, posts, comments);
  });

Async/Await : la syntaxe moderne

async/await est une syntaxe plus lisible pour travailler avec les Promises.

// Avec Promises
function fetchData() {
  return fetch('/api/data')
    .then(r => r.json())
    .then(data => console.info(data));
}

// Avec async/await
async function fetchData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  console.info(data);
}
À retenir : async marque une fonction comme asynchrone. await pause l'exécution jusqu'à ce que la Promise soit résolue.

Promises vs Async/Await : comparaison

Aspect Promises Async/Await
Lisibilité Chaînes de .then() Code synchrone-like
Erreurs .catch() try/catch
Parallélisation Promise.all() Promise.all()
Debugging Stacktraces moins claires Stacktraces claires

Gestion des erreurs

Avec Promises :

fetch('/api/data')
  .then(r => r.json())
  .catch(error => {
    console.error('Erreur:', error.message);
  });

Avec Async/Await :

async function getData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Erreur:', error.message);
    return null;
  }
}

Patterns et cas d'usage avancés

Pattern 1 : Retry avec backoff exponentiel

// Retry automatique avec délai croissant
async function fetchWithRetry(url, options = {}, maxAttempts = 3) {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            const response = await fetch(url, options);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            return await response.json();
        } catch (error) {
            if (attempt === maxAttempts) throw error;

            // Délai exponentiel : 1s, 2s, 4s...
            const delay = Math.pow(2, attempt - 1) * 1000;
            console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms`);
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

// Usage
const data = await fetchWithRetry('/api/data', {}, 3);

Pattern 2 : Timeout avec AbortController

// Annuler une requête si elle dépasse un délai
async function fetchWithTimeout(url, timeoutMs = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return await response.json();
    } catch (error) {
        if (error.name === 'AbortError') {
            throw new Error(`Request timeout after ${timeoutMs}ms`);
        }
        throw error;
    } finally {
        clearTimeout(timeoutId);  // Toujours nettoyer le timer
    }
}

Promise.allSettled() — résultats même en cas d'erreur

// allSettled() attend TOUTES les promises, même les rejetées
const results = await Promise.allSettled([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/broken-endpoint').then(r => r.json())  // Échouera
]);

// Chaque résultat a un status 'fulfilled' ou 'rejected'
results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
        console.info(`API ${index}: OK`, result.value);
    } else {
        console.error(`API ${index}: ERREUR`, result.reason);
    }
});

// Extraire uniquement les succès
const successes = results
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);
Méthode Comportement sur erreur Cas d'usage
Promise.all()Rejette dès la première erreurDonnées interdépendantes
Promise.allSettled()Attend toutes, retourne statutsDonnées indépendantes avec fallback
Promise.race()Première qui résout/rejette gagneTimeout, cache vs réseau
Promise.any()Première résolue gagne, ignore les rejetsRedondance, multi-CDN

Promise.race() — course entre promesses

// Utiliser le cache local si plus rapide que le réseau
function fetchFromCacheOrNetwork(key, networkUrl) {
    const cachePromise = caches.open('v1').then(cache => cache.match(key));
    const networkPromise = fetch(networkUrl).then(r => r.json());

    return Promise.race([cachePromise, networkPromise]);
}

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

// Usage
const data = await withTimeout(fetchData(), 3000);

Async generators — flux de données

// Async generator : yield des valeurs asynchrones
async function* fetchPages(baseUrl, maxPages = 5) {
    for (let page = 1; page <= maxPages; page++) {
        const response = await fetch(`${baseUrl}?page=${page}`);
        const data = await response.json();

        if (data.items.length === 0) return;  // Arrêt si page vide
        yield data.items;
    }
}

// Consommation avec for await...of
async function loadAllArticles() {
    const allArticles = [];

    for await (const pageItems of fetchPages('/api/articles')) {
        allArticles.push(...pageItems);
        console.info(`Chargés: ${allArticles.length} articles`);
    }

    return allArticles;
}

// Streaming de résultats
async function streamSearch(query) {
    for await (const batch of fetchPages(`/api/search?q=${query}`)) {
        renderBatch(batch);  // Afficher au fur et à mesure
    }
}

TypeScript : typer l'asynchrone correctement

// Types pour les fonctions async
interface ApiResponse<T> {
    data: T;
    status: number;
    message: string;
}

// Fonction async typée
async function fetchUser(id: number): Promise<ApiResponse<User>> {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
}

// Gestion d'erreur typée
type Result<T, E = Error> =
    | { success: true; data: T }
    | { success: false; error: E };

async function safeApiFetch<T>(url: string): Promise<Result<T>> {
    try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data: T = await response.json();
        return { success: true, data };
    } catch (error) {
        return { success: false, error: error as Error };
    }
}

// Usage type-safe
const result = await safeApiFetch<User[]>('/api/users');
if (result.success) {
    result.data.forEach(user => console.info(user.name));
} else {
    console.error(result.error.message);
}

Conclusion

Async/await est la syntaxe à préférer pour la lisibilité, mais les méthodes statiques de Promise (all, allSettled, race, any) restent indispensables pour les patterns parallèles. En TypeScript, le type Result<T> rend la gestion d'erreur explicite et type-safe sans exceptions.

Règle d'or : Ne jamais ignorer une Promise — toujours await ou .catch(). Une Promise non gérée peut causer des bugs silencieux difficiles à débuguer.

Partager