Les promesses
Définition
Nous avons déjà utilisé des promesses sans le savoir lorsque nous avons manipulé fetch
l'année dernière.
fetch(url, options)
//pour faire le premier .then, on attend que la promesse renvoyée par fetch soit résolue
//avec la réponse du serveur.
.then(response => response.json())
//pout faire le second .then, on attend que le précédent soit résolu (et ainsi de suite)
.then(result => /* process result */)
.catch(error => {
console.error('Error:', error);
});
// le console.log("coucou"); sera exécuté immédiatement après le lancement de la requête fetch(),
// sans attendre la fin de la requête.
console.log("coucou");
Une autre façon d'écrire cet enchainement de codes asynchrones consiste à utiliser await
. De cette façon, le code patiente jusqu'à ce que la promesse soit réglée et la valeur de résolution de la promesse est fournie comme valeur de retour, ou alors la valeur d'échec déclenche une erreur.
try {
let response = await fetch(url, options); // se résout avec des en-têtes de réponse
// La ligne ci-dessous ne sera exécutée que si la réponse est arrivée.
let result = await response.json(); // lit le corps en tant que JSON
// La ligne ci-dessous ne sera exécutée que si la réponse est arrivée.
console.log("coucou");
} catch (error) {
console.error('Error:', error);
}
Une promesse est une fonction à laquelle on passera deux arguments : resolve et reject. Cette fonction est exécutée immédiatement par l'implémentation de Promise qui fournit les fonctions resolve et reject (elle est exécutée avant que le constructeur Promise ait renvoyé l'objet créé). Les fonctions resolve et reject, lorsqu'elles sont appelées, permettent respectivement de tenir ou de rompre la promesse. On attend de l'exécuteur qu'il démarre un travail asynchrone puis, une fois le travail terminé, appelle la fonction resolve (si tout s'est bien passé) ou la fonction reject (lorsqu'il y a eu un problème) pour définir l'état final de la promesse. Si une erreur est générée par l'exécuteur, la promesse est rompue et la valeur de retour de l'exécuteur est ignorée. (définition mozilla). Le schéma ci-dessous détaille les différents états d'une promesse.
Coder ses propres fonctions asynchrones
Vous aurez parfois besoin d'écrire vos propres fonctions asynchrones car (entre autres) :
- Les fonctions asynchrones peuvent être combinées et composées plus facilement en utilisant des promesses. Vous pouvez chainer des opérations asynchrones avec des
.then()
et gérer plusieurs promesses avec des méthodes telles quePromise.all()
. - elles sont compatibles avec
async
/await
: l'utilisation de promesses est la base de l'utilisation des mots-clés async et await, qui simplifient davantage la syntaxe des fonctions asynchrones.async
permet de déclarer une fonction asynchrone, etawait
permet d'attendre la résolution d'une promesse à l'intérieur de cette fonction.
const promise1 = new Promise(function(resolve, reject) {
//L'asynchronisme est simulé par le timeout aléatoire.
//dans un exemple réel, ce pourrait être un fetch avec une réponse
// un peu lente de la part du serveur
setTimeout(function() {
resolve('foo');
}, Math.random() * 2000 + 1000);
});
//Quand la promesse est tenue c'est ce code (ce qui suit then) qui sera exécuté. resolve est "lié" à then.
promise1.then(function(value) {
console.log(value);
// "foo"
});
//exécution de la promesse
promise1;
Le même code avec await
et sync
:
const promise2 = new Promise((resolve, reject) => {
// L'asynchronisme est simulé par le timeout aléatoire.
setTimeout(() => {
resolve('foo');
}, Math.random() * 2000 + 1000);
});
const executePromise = async () => {
try {
// Utiliser await pour attendre que la promesse soit résolue
const value = await promise2;
console.log(value);
// expected output: "foo"
} catch (error) {
console.error('Error:', error);
}
};
// Appeler la fonction asynchrone
executePromise();
Dans l'exemple ci-dessous, on écrit un code qui gère un rejet de la promesse.
const methode1 = () => {
return new Promise((resolve, reject) => {
//peut être un chargement d'une grosse image, d'un script, une requette XHR
console.log("Methode 1 fait un truc asynchrone");
// réussir une fois sur deux
if (Math.random() > .5) {
resolve("Tout va bien dans la méthode asynchrone 1");
} else {
reject(Error('Problème méthode 1'));
}
})
}
const methode2 = (reponse) => {
console.log(reponse);
return new Promise((resolve) => {
//peut être un chargement d'une grosse image, d'un script, une requette XHR
console.log("Methode 2 fait un truc asynchrone");
// 100% de réussite
resolve("Tout va bien dans la méthode asynchrone 2");
})
}
methode1()
.then((reponse) => methode2(reponse))
.then((reponse)=> console.log(reponse,"!"))
.catch((alert)=>console.log(alert))
.then(() => console.log ("tout est terminé"));
Lorsque vous utilisez async
/await
, la fonction elle-même retourne une promesse. Si la fonction retourne une valeur, la promesse est résolue avec cette valeur. Si la fonction lance une exception, la promesse est rejetée avec l'exception lancée.
const methode1 = async () => {
console.log("Methode 1 fait un truc asynchrone");
if (Math.random() > 0.5) {
return "Tout va bien dans la méthode asynchrone 1";
} else {
throw new Error('Problème méthode 1');
}
};
const methode2 = async (reponse) => {
console.log(reponse);
console.log("Methode 2 fait un truc asynchrone");
return "Tout va bien dans la méthode asynchrone 2";
};
const main = async () => {
try {
const reponse1 = await methode1();
const reponse2 = await methode2(reponse1);
console.log(reponse2, "!");
} catch (error) {
console.error(error.message);
} finally {
console.log("tout est terminé");
}
};
// Appeler la fonction principale
main();
Exemple complet montrant l'importance de faire une fonction asynchrone
Nous allons ici utiliser l'API , où vous devez attendre une réponse avant de procéder à d'autres actions. Voici un exemple intéressant qui illustre l'utilisation des promesses pour rendre une fonction asynchrone et justifie pourquoi cela est nécessaire.
Cas d'Utilisation : Récupérer des Données Utilisateur
Nous souhaitons utiliser l'API REST jsonplaceholder
qui fournit des informations sur des utilisateurs. Nous voulons récupérer les détails d'un utilisateur spécifique (comme son nom, son email, et d'autres informations) à partir de cette API. Vous pouvez créer une fonction qui utilise fetch
pour faire cet appel API, mais vous voudrez que cette fonction soit asynchrone pour pouvoir gérer l'attente de la réponse.
Étapes
- Créer une fonction asynchrone pour récupérer les données.
- Utiliser
fetch
pour faire une requête à l'API. - Gérer les erreurs avec des promesses.
- Utiliser les données récupérées pour mettre à jour l'interface utilisateur ou effectuer d'autres actions.
Exemple de Code
Voici un exemple concret :
const getUserById = async (userId) => {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
// Vérifiez si la réponse est correcte
if (!response.ok) {
throw new Error(`Erreur HTTP : ${response.status}`);
}
// Parsez la réponse JSON
const user = await response.json();
return user; // Retournez l'utilisateur
} catch (error) {
console.error('Erreur lors de la récupération de l\'utilisateur:', error);
throw error; // Relancez l'erreur pour une gestion ultérieure
}
}
// Utilisation de la fonction asynchrone
getUserById(1)
.then(user => {
console.log('Détails de l\'utilisateur:', user);
// Mettez à jour l'interface utilisateur ou effectuez d'autres actions avec les données
})
.catch(error => {
console.error('Erreur capturée lors de l\'appel à getUserById:', error);
});
getUserById(-9851)
.then(user => {
console.log('Détails de l\'utilisateur:', user);
// Mettez à jour l'interface utilisateur ou effectuez d'autres actions avec les données
});
.catch(error => {
console.error('Erreur capturée lors de l\'appel à getUserById:', error);
});
Explication
Asynchrone : La fonction
getUserById
est déclarée avecasync
, ce qui permet d'utiliserawait
à l'intérieur. Cela rend le code plus lisible et évite lesthen
imbriqués.Requête API : On utilise
fetch
pour faire une requête à une API REST. La réponse est attendue de manière asynchrone grâce àawait
.Gestion des Erreurs : Si la réponse n'est pas correcte (
!response.ok
), une erreur est lancée, ce qui permet de capturer et de gérer les erreurs de manière centralisée.Utilisation des Données : La promesse retournée par
getUserById
est ensuite utilisée pour manipuler les données utilisateur, par exemple, pour les afficher sur l'interface utilisateur.
Il y a une forte probabilité que l'affichage générant une erreur (car id inconnu) s'affiche avant celui de l'utilisateur 1. Il faut bien comprendre que les deux appels sont asynchrones. La réponse de la deuxième requête peut arriver avant le retour de la première.
Si vous voulez gérer les réponses de manière synchrone, il faut utiliser Promise.all()
qui permet de d'effectuer le then
uniquement lorsque tous les resolve
des méthodes asynchrones sont retournés.
Attention : si une des méthodes retourne un reject
alors l'erreur est récupérée dans le catch
et les retours des autres méthodes asynchrones sont perdus.
Promise.all([getUserById(1), getUserById(2), getUserById(3),])
.then(([user1, user2, user3]) => {
//le code ci-dessous est exécuté uniquement
//quand les 3 appels à getUserById sont
console.log('Détails de l\'utilisateur 1:', user1);
console.log('Détails de l\'utilisateur 2:', user2);
console.log('Détails de l\'utilisateur 2:', user3);
})
.catch(error => {
console.error('Une des requêtes a échoué :', error);
});
Pourquoi utiliser une Promesse ?
Contrôle de Flux : En rendant la fonction asynchrone, vous vous assurez que le code qui dépend des résultats de l'API ne s'exécute qu'après que les données ont été récupérées.
Gestion des Erreurs : Les promesses permettent une gestion des erreurs plus propre, en centralisant la logique d'erreur dans un
catch
.Lisibilité : Le code devient plus lisible, ce qui est essentiel lors de l'écriture de code asynchrone, surtout lorsqu'il y a plusieurs opérations qui dépendent les unes des autres.
Exercice : récupération des données d'utilisateurs et de leurs publications
Contexte
Vous devez créer une petite application qui récupère des données d'utilisateurs et leurs publications depuis une API. Pour chaque utilisateur, vous devrez récupérer les détails et, ensuite, récupérer toutes les publications de cet utilisateur. L'API que vous pouvez utiliser est JSONPlaceholder, qui fournit des exemples de données.
Étapes à suivre
- Récupérer les utilisateurs : Utilisez l'API
https://jsonplaceholder.typicode.com/users
pour récupérer une liste d'utilisateurs. - Pour chaque utilisateur, récupérez leurs publications en utilisant l'API
https://jsonplaceholder.typicode.com/posts?userId={userId}
. - Utilisez
Promise.all()
pour attendre que toutes les promesses de récupération de publications soient résolues avant d'afficher les résultats. - Affichez le nom de l'utilisateur et le titre de chaque publication associée à cet utilisateur dans la console.
Je vous donne une partie du code pour vous guider vers le bonne solution.
Je veux aussi que vous commenciez à prendre de bonnes habitudes.
Par exemple, il faut prendre l'habitude d'utilser map
si on souhaite transformer un tableau en un autre plutôt et un forEach
si on souhaite simplement itérer sans retourner de valeur (par exemple l'afficher)
const fetchUsers = async () => {
/*À vous de jouer*/
return response.json();
};
const fetchPostsByUserId = async (userId) => {
/*À vous de jouer*/
return response.json();
};
const displayUserPosts = async () => {
try {
// 1. Récupérer des utilisateurs
const users = await fetchUsers();
// 2. Pour chaque utilisateur, récupérer ses publications
const postPromises = users.map(/*À vous de jouer*/);
// 3. Attendre que toutes les promesses soient résolues
const posts = await Promise.all(/*À vous de jouer*/);
// 4. Afficher les résultats
users.forEach((user, index) => {
/*À vous de jouer*/
});
} catch (error) {
console.error('Une erreur s\'est produite:', error);
}
};
// Appeler la fonction pour afficher les utilisateurs et leurs publications
displayUserPosts();