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 :

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

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

var filesResults = [];

try{
	files.forEach(function (fileName, index) {
		fs.readFile('dossier/' + fileName, { encoding: 'utf8' }, function (err, fileContent) {
			if (!err || !fileContent) {
				throw 'Fichier illisible';
			}
			
			var fileJson = JSON.parse(fileContent);
			filesResults[index] = fileJson;
			
			if (filesResults.length === files.length) { // On regarde si tous les fichiers ont été lus.
				filesResults.forEach(function (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.

À propose 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 Promise

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(function (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(function (resolve, reject) {
		var xhr = new XMLHttpRequest();
		
		xhr.onload = function (event) {
			resolve(xhr.responseText); // Si la requête réussit, on résout la promesse en indiquant le contenu du fichier
		};
		
		xhr.onerror = function (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(function (content) {
	console.info('Fichier chargé !');
	console.log(content);
}).catch(function (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(function (content) {
	console.info('Fichier chargé !');
	console.log(content);
}, function (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(function (data) {
	return new Promise(function (resolve, reject) {
		fs.writeFile('test-bis.txt', data, function (err) {
			if (err) {
				reject('Impossible d\'écrire dans le second fichier');
				return;
			}
			
			resolve(data);
		});
	});
}).then(function (data) {
	console.info('Le contenu du premier fichier a été écrit dans le second');
}).catch(function (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(function (data) {
	// La variable data correspond ici au contenu du premier fichier
	return loadDistantFile('test-2.txt'); // On retourne donc une nouvelle promesse
}).then(function (data) {
	// La variable data correspond donc au contenu du second fichier
	console.info('Le contenu du second fichier a été chargé');
}).catch(function (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(function (data) {
	console.dir(data); // On envoie notre JSON déjà parsé dans la console
}).catch(function (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

var 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)

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

var promesses = [];

files.forEach(function (fileName{
	var 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(function (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(function (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 :

var 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)

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

var promesses = [];

scripts.forEach(function (scriptName{
	var ma_promesse = loadDistantFile('scripts/' + scriptName);
	promesses.push(ma_promesse);
});

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

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.

Et si vous cherchez des exemples d’implémentations, la fonction fetch est un très bon exemple !

Vous pouvez retrouver la version originale de ce tutoriel, rédigée en mars 2015, sur Zeste de Savoir.