Les promesses en JavaScript

Adieu l'enfer des callbacks !

Si vous avez suivi les nouveautés de la norme EcmaScript 6 — plus communément appelée ES6 — vous savez sans doute qu’un ajout non négligeable a été fait au sein du langage JavaScript : les promesses (Promise pour les intimes).

Concrètement, les promesses vont permettre plusieurs choses :

  • Ne plus se perdre dans les callbacks imbriqués
  • Pouvoir faire des traitements asynchrones de manière simultanée tout en récupérant les résultats une seule fois simplement

Par exemple, si vous souhaitez lire plusieurs fichiers JSON avec Node.js, mais que vous souhaitez les traiter en même temps, avant vous auriez fait quelque chose comme ça :

const fs = require('fs'); // On charge le module filesystem classique

const files = ['fichier-1.json', 'fichiers-2.json', 'fichiers-3.json', 'fichiers-4.json'];

let filesResults = [];

try{
	files.forEach((fileName, index) => {
		fs.readFile('dossier/' + fileName, { encoding: 'utf8' }, (err, fileContent) => {
			if (!err || !fileContent) {
				throw new Error('Fichier illisible');
			}
			
			const fileJson = JSON.parse(fileContent);
			filesResults[index] = fileJson;
			
			if (filesResults.length === files.length) { // On regarde si tous les fichiers ont été lus.
				filesResults.forEach((fileJson, index) => {
					console.log(`Contenu du fichier ${index}`);
					console.dir(fileJson);
				});
			}
		});
	});
}
catch (Exception err) {
	console.error(`Erreur lors de la lecture d'un fichier`);
}

En lisant le tutoriel, vous verrez qu’avec les promesses le code deviendra beaucoup plus clair, par exemple en passant de 5 niveaux d’indentation à seulement 2. Le code sera donc plus léger et on évitera de mélanger la lecture des fichiers avec la condition qui détermine s’ils ont tous été lus.

Mais ce n’est pas tout ! Vous verrez aussi que l’on peut faire des choses assez puissantes avec les requêtes, tout en gardant un code propre et agréable à lire.

À propos des polyfills

Il y a quelques années, quand j’ai rédigé la première version de ce tutoriel, il était souvent nécessaire d’utiliser un polyfill pour importer la class Promise. Ce n’est aujourd’hui que rarement le cas. Je vous laisse donc décider de la marche à suivre si besoin…

Créer notre propre promesse

Pour mieux comprendre comment une promesse fonctionne, le plus simple est d’en créer une. C’est en plus une base qui pourra vous servir assez régulièrement.

Pour l’exercice nous allons donc travailler côté client en apprenant à charger un fichier distant.

Créons une promesse

 function loadDistantFile(url) {
	return new Promise((resolve, reject) => {
		
	});
}

La fonction ne fait pas encore grand-chose, mais vous pouvez déjà voir qu’elle renvoie une promesse. Et vous pouvez aussi apercevoir deux variables — resolve et reject — qui vont permettre de déterminer si la promesse est résolue (le boulot a été fait sans accroc) ou si elle a échoué (un problème est survenu).

Une requête basique

function loadDistantFile(url) {
	return new Promise((resolve, reject) => {
		var xhr = new XMLHttpRequest();
		
		xhr.onload = (event) => {
			resolve(xhr.responseText); // Si la requête réussit, on résout la promesse en indiquant le contenu du fichier
		};
		
		xhr.onerror = (err) => {
			reject(err); // Si la requête échoue, on rejette la promesse en envoyant les infos de l'erreur
		}
		
		xhr.open('GET', url, true);
		xhr.send(null);
	});
}

Utiliser la requête

Pour utiliser une promesse, il n’y a rien de plus simple : il y a deux méthodes then et catch pour gérer les possibilités :

loadDistantFile('test.txt').then((content) => {
	console.info('Fichier chargé !');
	console.log(content);
}).catch((err) => {
	console.error('Erreur !');
	console.dir(err);
});

Sachez que vous pouvez aussi n’utiliser que la méthode then en lui passant deux paramètres. Le second paramètre sera alors la fonction à appeler en cas d’erreur :

loadDistantFile('test.txt').then((content) => {
	console.info('Fichier chargé !');
	console.log(content);
}, (err) => {
	console.error('Erreur !');
	console.dir(err);
});

Enchaîner les traitements

L’un des avantages des promesses est de pouvoir enchaîner les traitements. La méthode then est très utile dans ce cas, car elle renvoie une nouvelle promesse.

On peut donc très bien écrire le contenu de notre fichier dans un autre :

loadDistantFile('test.txt').then((data) => {
	return new Promise((resolve, reject) => {
		fs.writeFile('test-bis.txt', data, (err) => {
			if (err) {
				reject(`Impossible d'écrire dans le second fichier`);
				return;
			}
			
			resolve(data);
		});
	});
}).then((data) => {
	console.info('Le contenu du premier fichier a été écrit dans le second');
}).catch((err) => {
	console.error(err);
});

On peut aussi charger des fichiers les uns après les autres de la même manière :

loadDistantFile('test.txt').then((data) => {
	// La variable data correspond ici au contenu du premier fichier
	return loadDistantFile('test-2.txt'); // On retourne donc une nouvelle promesse
}).then((data) => {
	// La variable data correspond donc au contenu du second fichier
	console.info('Le contenu du second fichier a été chargé');
}).catch((err) => {
	console.error(err);
});

Mais les promesses ne se cantonnent pas aux traitements asynchrones ! Vous pouvez très bien les utiliser pour des traitements synchrones.

Par exemple, en reprenant notre requête précédente, on peut aussi tout simplement parser du JSON :

loadDistantFile('text.json').then(JSON.parse).then((data) => {
	console.dir(data); // On envoie notre JSON déjà parsé dans la console
}).catch((err) => {
	console.error(err); // Oups !
});

Gérer des traitement simultanés

Chose promise, chose due !

J’ai résolu pour vous la promesse faite dans l’introduction du tutoriel ! :P

const fsp = require('fs-promise'); // On charge le module filesystem dans sa version à base de promesses (il s'agit d'un module npm indépendant, attention à ne pas vous mélanger les pinceaux)

const files = ['fichier-1.json', 'fichiers-2.json', 'fichiers-3.json', 'fichiers-4.json'];

let promesses = [];

files.forEach((fileName) => {
	const ma_promesse = fsp.readFile('dossier/' + fileName, { encoding: 'utf8' }).then(JSON.parse); // On demande une promesse sur la lecture du fichier
	promesses.push(ma_promesse);
});

Promise.all(promesses).then((data) => {
	console.info('Tous les fichiers ont été lus avec succès');
	data.forEach(function(fileJson, index) {
		console.log(`Contenu du fichier ${index}`);
		console.dir(fileJson);
	});
}).catch((err) => {
	console.error('Une erreur est survenue lors de la lecture des fichiers');
});

Comment ça marche ?

Si vous lisez le code, vous verrez que je n’utilise pas new Promise() mais Promise.all(promesses). Cette fonction renvoie en réalité une promesse qui ne sera résolue que lorsque toutes les promesses passées en paramètre (qui doit être un itérable, par exemple un tableau) sont elles-mêmes résolues, et qui échoue lorsque l’une d’elles (peu importe laquelle) échoue.

Faisons la course !

Maintenant que vous savez gérer des traitements simultanés, nous allons voir comment gérer une situation assez similaire mais pour laquelle le comportement diffère légèrement : la course.

Imaginons par exemple que vous cherchiez à savoir quel script répond le plus vite à une requête. On se fiche donc un peu du résultat des serveurs les plus lents : on veut celui du plus rapide.

On prendra donc pour l’exemple des fichiers PHP qui attendent tous un temps différent (via la fonction sleep ou usleep par exemple) puis renvoient leur nom. Par exemple :

<?php
sleep(2); // J'attends 2 secondes (à adapter pour chaque script)
echo 'Numéro 1 !'; // Je dis mon nom

Ensuite en JavaScript je leur demande de faire la course :

const fsp = require('fs-promise'); // On charge le module filesystem dans sa version à base de promesses (il s'agit d'un module npm indépendant, attention à ne pas vous mélanger les pinceaux)

const scripts = ['script-1.php', 'script-2.php', 'script-3.php', 'script-4.php'];

let promesses = [];

scripts.forEach((scriptName) => {
	const ma_promesse = loadDistantFile(`scripts/${scriptName}`);
	promesses.push(ma_promesse);
});

Promise.race(promesses).then((resultat) => {
	console.info('On a un gagnant !');
	console.log(resultat);
}).catch((err) => {
	console.error(`Une erreur est survenue lors de l'accès aux scripts`);
});

Bonus : quelques exemples d'implémentation

Comme vous savez maintenant comment fonctionnent les promesses, vous vous dites peut-être qu’il serait temps de voir concrètement ce que ça donne. Rien de tel pour ça que des exemples d’utilisation ! :)

L’API fetch

Cette API permet concrètement de remplacer nos bonnes vieilles XMLHttpRequest par une simple fonction fetch qui renvoie une promesse. Cette fonction permet tout simplement de récupérer un fichier distant, quel que soit son type.

Créez vos propres Promises

Le site promisejs.org propose d'étudier le code utilisé pour créer un polyfill de Promise. Vous pouvez y jeter un œil pour mieux comprendre le fonctionnement des promesses.

Aller encore plus loin avec async/await

Maintenant que vous maitrisez les Promise… et si je vous disais qu’on peut dorénavant encore plus simplifier notre code ?!

Eh oui, grâce à deux mots-clés vous pouvez déclarer une fonction asynchrone (comme un Promise) et attendre son résultat (comme un then) !

Reprenons par exemple l’exemple du chargement d’un fichier JSON :

try {
	const content = await loadDistantFile('test.json');
	const data = JSON.parse(content);

	console.dir(data); // On envoie notre JSON parsé dans la console
} catch (err) {
	console.error(err); // Oups !
}

Le bloc try/catch permet de gérer les erreurs : ce n’est pas obligatoire mais généralement une bonne idée de le faire ;)

Le mot-clé await doit être utilisé dans un module JS ou dans une fonction asynchrone, et non directement à la racine de votre script.

Si vous voulez exécuter cet exemple directement il faudra alors l’inclure dans une fonction asynchrone :

// On déclare notre fonction asynchrone
const letsgo = async () => {
	// Insérer le code ici
}

// On l'exécute
letsgo();

C’est encore un peu flou ? Pas de souci, revenons à un exemple plus concret !

Imaginons que l’on veuille charger plusieurs fichiers à la suite (ou, dans le monde réel, appeler plusieurs APIs et/ou effectuer plusieurs requête dans une base de données) de façon séquentielle :

const fichier1 = await loadDistantFile('test.txt');
const fichier2 = await loadDistantFile('test-2.txt');
const fichier3 = await loadDistantFile('test-3.txt');

console.dir({ fichier1, fichier2, fichier3 });

Alors qu’avec la syntaxe traditionnelle, ça donnerait plutôt ça :

let fichier1, fichier2, fichier3; // On initialise nos variables dans le bon scope

loadDistantFile('test.txt').then((data) => {
	fichier1 = data;

	return loadDistantFile('test-2.txt');
}).then((data) => {
	fichier2 = data;

	return loadDistantFile('test-3.txt');
}).then((data) => {
	fichier3 = data;

	console.dir({ fichier1, fichier2, fichier3 });
});

3 lignes au lieu de 10 ! C’est tout de suite plus lisible qu’avant, non ? ^^

En résumé, le mot-clé async permet de déclarer qu’une fonction est asynchrone (elle renvoie donc une Promise (ou directement un résultat si on le souhaite), et await permet d’attendre le résultat d’une fonction asynchrone (ou d’une Promise) avant de passer à l’instruction suivante.


Et voilà ! Vous savez maintenant tout (ou presque) sur les promesses en JavaScript !

N’hésitez pas à tester et à créer vos propres ressources à base de promesses.

4 commentaires

Eh bien il existe un troisième paramètre, après resolve et reject, qui s’appelle notify pour gérer cela !

AngularJS a cette extension, mais pas Node.js :

1
2
> new Promise((...args) => console.log(args))
[ [Function], [Function] ]

…parce que ce n’est pas standard.


Ah, et autre chose : Les Promises sont supportées partout sauf sur IE11 et autres navigateurs moisis et rares. Il n’y a pratiquement plus besoin de polyfills.

+2 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte