0%
Modern JavaScript: Promises and Async/Await

Promises & Async

Modern Asynchronous Programming

10-15 min

Modern JavaScript: Promises and Async/Await

Asynchronous programming is at the heart of modern JavaScript. Promises and async/await are essential tools for handling asynchronous operations in an elegant and maintainable way.

Promises

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation.

Creating a Promise

const maPromesse = new Promise((resolve, reject) => {
    // Opération asynchrone
    setTimeout(() => {
        const succes = true;
        if (succes) {
            resolve("Opération réussie !");
        } else {
            reject(new Error("L'opération a échoué"));
        }
    }, 1000);
});

// Utilisation
maPromesse
    .then(resultat => console.log(resultat))
    .catch(erreur => console.error(erreur));

Promise States

A Promise can be in one of the following three states:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: The operation succeeded
  • Rejected: The operation failed
const promessePending = new Promise(() => {});
console.log(promessePending); // Promise { <pending> }

const promesseFulfilled = Promise.resolve("Succès");
console.log(promesseFulfilled); // Promise { <fulfilled>: "Succès" }

const promesseRejected = Promise.reject("Erreur");
console.log(promesseRejected); // Promise { <rejected>: "Erreur" }

Promise Chaining

Promises can be chained to run sequential operations:

function obtenirUtilisateur(id) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id, nom: "Alice" });
        }, 1000);
    });
}

function obtenirCommandes(utilisateur) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({
                utilisateur,
                commandes: [
                    { id: 1, produit: "Livre" },
                    { id: 2, produit: "Ordinateur" }
                ]
            });
        }, 1000);
    });
}

// Chaînage
obtenirUtilisateur(1)
    .then(utilisateur => obtenirCommandes(utilisateur))
    .then(resultat => console.log(resultat))
    .catch(erreur => console.error(erreur));

Useful Promise Methods

Promise.all()

Waits for all promises to be fulfilled:

const promise1 = Promise.resolve(3);
const promise2 = new Promise(resolve => setTimeout(() => resolve(42), 1000));
const promise3 = fetch('https://api.example.com/data');

Promise.all([promise1, promise2, promise3])
    .then(resultats => {
        console.log(resultats); // [3, 42, Response]
    })
    .catch(erreur => {
        console.error("Une des promises a échoué:", erreur);
    });

Promise.race()

Returns the first promise to be fulfilled or rejected:

const promise1 = new Promise(resolve => setTimeout(() => resolve("un"), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve("deux"), 500));

Promise.race([promise1, promise2])
    .then(resultat => console.log(resultat)); // "deux"

Promise.allSettled()

Waits for all promises to settle (fulfilled or rejected):

const promises = [
    Promise.resolve(1),
    Promise.reject("erreur"),
    Promise.resolve(3)
];

Promise.allSettled(promises)
    .then(resultats => {
        resultats.forEach(resultat => {
            if (resultat.status === "fulfilled") {
                console.log("Succès:", resultat.value);
            } else {
                console.log("Échec:", resultat.reason);
            }
        });
    });

Async/Await

async/await is a more elegant syntax for working with Promises.

Async Functions

async function obtenirDonnees() {
    try {
        const utilisateur = await obtenirUtilisateur(1);
        const commandes = await obtenirCommandes(utilisateur);
        return commandes;
    } catch (erreur) {
        console.error("Erreur:", erreur);
        throw erreur;
    }
}

// Utilisation
obtenirDonnees()
    .then(resultat => console.log(resultat))
    .catch(erreur => console.error(erreur));

With Arrow Functions

const obtenirDonnees = async () => {
    const utilisateur = await obtenirUtilisateur(1);
    const commandes = await obtenirCommandes(utilisateur);
    return commandes;
};

Parallel Execution with async/await

async function obtenirTout() {
    try {
        // Exécution parallèle
        const [utilisateurs, produits] = await Promise.all([
            obtenirUtilisateurs(),
            obtenirProduits()
        ]);
        
        return { utilisateurs, produits };
    } catch (erreur) {
        console.error("Erreur:", erreur);
        throw erreur;
    }
}

Asynchronous Loops

async function traiterUtilisateurs(utilisateurs) {
    // Séquentiel
    for (const utilisateur of utilisateurs) {
        await envoyerEmail(utilisateur);
    }
    
    // Parallèle
    const promesses = utilisateurs.map(utilisateur => envoyerEmail(utilisateur));
    await Promise.all(promesses);
}

Patterns and Best Practices

Retry Pattern

async function avecRetry(fn, maxTentatives = 3) {
    let dernierErreur;
    
    for (let tentative = 1; tentative <= maxTentatives; tentative++) {
        try {
            return await fn();
        } catch (erreur) {
            dernierErreur = erreur;
            console.log(`Tentative ${tentative} échouée`);
            if (tentative < maxTentatives) {
                await new Promise(resolve => 
                    setTimeout(resolve, 1000 * tentative)
                );
            }
        }
    }
    
    throw dernierErreur;
}

// Utilisation
const fetchAvecRetry = () => 
    avecRetry(() => fetch('https://api.example.com/data'));

Timeout for Promises

function avecTimeout(promesse, delai) {
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Timeout')), delai);
    });
    
    return Promise.race([promesse, timeoutPromise]);
}

// Utilisation
avecTimeout(fetch('https://api.example.com/data'), 5000)
    .then(response => response.json())
    .catch(erreur => console.error('Erreur ou timeout:', erreur));

Error Handling

async function gererErreurs() {
    try {
        const donnees = await fetch('https://api.example.com/data');
        
        if (!donnees.ok) {
            throw new Error(`HTTP error! status: ${donnees.status}`);
        }
        
        const resultat = await donnees.json();
        return resultat;
        
    } catch (erreur) {
        if (erreur instanceof TypeError) {
            console.error('Erreur réseau:', erreur);
        } else if (erreur.name === 'AbortError') {
            console.error('Requête annulée:', erreur);
        } else {
            console.error('Autre erreur:', erreur);
        }
        throw erreur;
    }
}

Practical Use Cases

HTTP Requests with fetch

async function obtenirDonneesAPI() {
    try {
        const response = await fetch('https://api.example.com/data', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ key: 'value' })
        });
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        return data;
        
    } catch (error) {
        console.error('Erreur:', error);
        throw error;
    }
}

Loading Images

function chargerImage(url) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = () => reject(new Error(`Échec du chargement de ${url}`));
        img.src = url;
    });
}

async function chargerGalerie(urls) {
    try {
        const images = await Promise.all(
            urls.map(url => chargerImage(url))
        );
        return images;
    } catch (erreur) {
        console.error('Erreur de chargement:', erreur);
        throw erreur;
    }
}

Caching with Promises

class CachePromise {
    constructor() {
        this.cache = new Map();
    }
    
    async get(key, fetchFn) {
        if (this.cache.has(key)) {
            return this.cache.get(key);
        }
        
        const promise = fetchFn();
        this.cache.set(key, promise);
        
        try {
            await promise;
            return promise;
        } catch (erreur) {
            this.cache.delete(key);
            throw erreur;
        }
    }
}

// Utilisation
const cache = new CachePromise();

async function obtenirUtilisateur(id) {
    return cache.get(`utilisateur-${id}`, async () => {
        const response = await fetch(`/api/utilisateurs/${id}`);
        return response.json();
    });
}

Practice Exercises

  1. Simulating a queue
class FileAttente {
    constructor() {
        this.taches = [];
    }
    
    ajouterTache(tache) {
        return new Promise((resolve, reject) => {
            this.taches.push({ tache, resolve, reject });
            if (this.taches.length === 1) {
                this.executerSuivant();
            }
        });
    }
    
    async executerSuivant() {
        if (this.taches.length === 0) return;
        
        const { tache, resolve, reject } = this.taches[0];
        
        try {
            const resultat = await tache();
            resolve(resultat);
        } catch (erreur) {
            reject(erreur);
        } finally {
            this.taches.shift();
            this.executerSuivant();
        }
    }
}

// Utilisation
const file = new FileAttente();

file.ajouterTache(() => new Promise(resolve => 
    setTimeout(() => resolve("Tâche 1"), 1000)
));

file.ajouterTache(() => new Promise(resolve => 
    setTimeout(() => resolve("Tâche 2"), 500)
));
  1. Concurrent request manager
class GestionnaireRequetes {
    constructor(maxConcurrent = 3) {
        this.maxConcurrent = maxConcurrent;
        this.actives = 0;
        this.file = [];
    }
    
    async ajouter(fn) {
        if (this.actives >= this.maxConcurrent) {
            await new Promise(resolve => this.file.push(resolve));
        }
        
        this.actives++;
        
        try {
            return await fn();
        } finally {
            this.actives--;
            if (this.file.length > 0) {
                const suivant = this.file.shift();
                suivant();
            }
        }
    }
}

// Utilisation
const gestionnaire = new GestionnaireRequetes(2);

async function effectuerRequetes() {
    const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
    
    const promesses = urls.map(url => 
        gestionnaire.ajouter(() => fetch(url))
    );
    
    return Promise.all(promesses);
}

Conclusion

Promises and async/await are fundamental tools for handling asynchronicity in modern JavaScript:

  • Promises offer a structured approach to handling asynchronous operations
  • async/await simplifies the syntax and makes code more readable
  • Patterns like retry, timeout, and caching are essential for robust applications
  • Proper error handling is crucial for application reliability

In the next tutorial, we’ll explore ES modules and how to organize your JavaScript code effectively.


Need to brush up on object-oriented programming? Check out our previous tutorial on OOP.

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!