L’objet Promise (pour « promesse ») est utilisé pour réaliser des traitements de façon asynchrone. Une promesse représente une unique opération asynchrone qui n’a pas encore été complétée, mais qui est attendue dans le futur. MDN
Plus concrètement, les promesses sont des objets apportant une nouvelle syntaxe au javascript qui permet de mettre à plat ce qu’on appelle techniquement le “code spaghetti “.
// callback-based functions
function spaghettiCode() {
asyncStuff(function (err, res) {
if (err) { throw err; }
moreAsyncStuff(function (err, res) {
if (err) { throw err; }
andMoreAsyncStuff(function (err, res) {
if (err) { throw err; }
oneLastAsyncStuff(function (err, res) {
if (err) { throw err; }
});
});
});
});
}
// promise-based functions
function flatCode() {
asyncStuff()
.then(moreAsyncStuff)
.then(andMoreAsyncStuff)
.then(oneLastAsyncStuff)
.catch(function (err) {
throw err;
});
}
Les promesses en javascript sont définies par un standard nommé Promises/A+ (https://promisesaplus.com/). Il existe plusieurs librairies qui l’implémentent, plus ou moins fidèlement, telles que Q, RSVP.js ou Bluebird. Je vais cependant uniquement parler dans cet article de l’implémentation native des promesses apportées par l’ES6.
Commençons par quelques termes techniques. Une promesse peut avoir trois statuts différents : pending
, resolved
(ou fullfilled
) et rejected
. Il est possible d’instancier une promesse avec chacun de ces trois statuts.
Une promesse pending
attendra sa résolution ou sa réjection.
Une promesse resolved
appellera la fonction spécifiée avec la méthode then
.
Une promesse rejected
appellera la fonction spécifiée avec la méthode catch
.
var promise = new Promise(function (resolve, reject) {});
promise.then(function () {
console.log('ok');
// unreached code
});
promise.catch(function () {
console.log('ko');
// unreached code
});
// display nothing
Une new Promise
est résolue lorsque resolve
est appelé.
var promise = new Promise(function (resolve, reject) {
resolve();
});
promise.then(function () {
console.log('ok');
});
promise.catch(function () {
console.log('ko');
// unreached code
});
// display:
// ok
Une new Promise
est rejetée si reject
est appelé, ou si throw
est invoqué.
var promise = new Promise(function (resolve, reject) {
reject();
});
promise.then(function () {
console.log('ok');
// unreached code
});
promise.catch(function () {
console.log('ko');
});
// display:
// ko
var resolvedPromise = Promise.resolve();
promise.then(function () {
console.log('ok');
});
promise.catch(function () {
console.log('ko');
// unreached code
});
// display:
// ok
var rejectedPromise = Promise.reject();
promise.then(function () {
console.log('ok');
// unreached code
});
promise.catch(function () {
console.log('ko');
});
// display:
// ko
Les fonctionnalités natives de Javascript/Node.JS ne sont pas encore promise-based. Transformer des fonctions callback-based en fonctions promise-based est donc un bon entraînement et une étape primordiale dans le développement d’un projet promise-based.
function setTimeoutPromise (duration) {
return new Promise(function (resolve) {
setTimeout(resolve, duration);
});
}
// usage:
setTimeoutPromise(1000)
.then(function () {
console.log('done !');
});
En Node.JS, il existe des librairies qui se chargeront de convertir automatiquement des fonctions callback-based en fonctions promise-based, comme par exemple https://github.com/digitaldesignlabs/es6-promisify.
Il y a quelques points importants à connaître à propos de l’enchaînement des promesses.
new Promise(function (resolve, reject) {})
retourne une promesse.
promise.then(function () {})
retourne une nouvelle promesse qui sera :
Résolue au retour de la fonction spécifiée en paramètre, ou si la fonction retourne une promesse résolue.
Rejetée si un throw
est appelé dans la fonction, ou si celle-ci retourne une promesse rejetée.
Lorsque la fonction spécifiée en paramètre de then
retourne une promesse, sa résolution sera attendue avant la suite de l’enchaînement.
var promise = Promise.resolve();
promise.then(function () {
console.log(1);
})
.then(function () {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(2);
resolve();
}, 0);
});
})
// wait for resolve to be called by setTimeout
.then(function () {
console.log(3);
});
// display:
// 1
// 2
// 3
Dans l’exemple suivant, les promesses ne sont pas correctement chaînées, toutes les fonctions sont associées au then
de la première promesse. Ces fonctions vont donc êtres appelées simultanément à la résolution de la promesse initiale.
var simultaneousPromise = Promise.resolve();
simultaneousPromise.then(function () {
console.log(1);
});
simultaneousPromise.then(function () {
return new Promise(function (resolve) {
setTimeout(function() {
console.log(2);
resolve();
}, 0);
});
});
simultaneousPromise.then(function () {
console.log(3);
});
// enchaînement brisé, promesses simultanées
// 1
// 3
// 2
Dans l’exemple suivant, l’enchaînement est préservé grâce à la conservation de chaque nouvelle promesse.
var chainedPromise = Promise.resolve();
chainedPromise = chainedPromise.then(function () {
console.log(1);
});
chainedPromise = chainedPromise.then(function () {
return new Promise(function (resolve) {
setTimeout(function() {
console.log(2);
resolve();
}, 0);
});
});
chainedPromise = goodPromise.then(function () {
console.log(3);
});
// enchaînement préservé
// 1
// 2
// 3
Il est possible de transmettre une unique variable à travers une promesse.
var promise = Promise.resolve(42, 41, 40);
promise.then(function () {
console.log(arguments);
// display: [42]
return 'hello';
}).then(function () {
console.log(arguments);
// display: ['hello']
return Promise.resolve({ foo: 'bar' });
}).then((function () {
console.log(arguments);
// display: [{ foo: 'bar' }]
});
La méthode catch
permet d’intercepter les promesses rejetées.
A la suite d’un catch
, l’enchaînement est préservé.
var promise = Promise.resolve();
promise.then(function () {
console.log('a');
throw new Error();
console.log('b');
})
.catch(function (err) {
console.log('err');
});
.then(function () {
console.log('c');
});
// display:
// a
// err
// c
Il est également possible de spécifier une fonction de catch
en la plaçant en deuxième argument de then
:
var promise = Promise.resolve();
promise.then(function () {
return Promise.reject(new Error());
})
.then(function thenFct() {
console.log('ok');
}, function catchFct(err) {
console.log('ko');
})
.then(function () {
console.log('done');
});
// display:
// ko
// done
Cependant, cette syntaxe fait perdre votre projet en lisibilité. À lire sur ce sujet.
Si plusieurs catch
sont présents, seul le premier sera appelé.
var promise = Promise.resolve();
promise.then(function () {
return Promise.reject(new Error());
})
.then(function () {
console.log('ok');
})
.catch(function (err) {
console.log('ko1');
})
.then(function () {
console.log('done');
})
.catch(function (err) {
console.log('ko2');
});
// display:
// ko1
// done
Enfin, vous pouvez transmettre l’exception au catch
suivant grâce à un throw
ou à un une promesse rejetée.
var promise = Promise.resolve();
promise.then(function () {
return Promise.reject(new Error());
})
.then(function () {
console.log('ok');
})
.catch(function (err) {
console.log('ko1');
throw err;
// or
return Promise.reject(err);
})
.then(function () {
console.log('done');
})
.catch(function (err) {
console.log('ko2');
});
// display:
// ko1
// ko2
Attention cependant, si vous invoquez throw
dans une fonction asynchrone, l’erreur ne sera pas catchée !
var promise = Promise.resolve();
promise.then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
throw new Error();
}, 0);
});
})
.then(function () {
console.log('done');
})
.catch(function (err) {
console.log('ko');
});
// will kill the program
Il n’est pas obligatoire, mais recommandé, de throw un objet Error
, car celui-ci embarque la stack d’exécution qui vous aidera à débugger.
promise.all
est une méthode prenant en paramètre un itérable (un Array par exemple) de promesses et renvoyant une promesse qui sera résolue lorsque toutes les promesses de l’itérable seront résolues.
Si l’une des promesses de l’itérable est rejetée, la promesse de promise.all
le sera également.
var p1 = Promise.resolve();
var p2 = new Promise(function (resolve) {
console.log('a');
resolve();
});
var p3 = new Promise(function (resolve) {
setTimeout(function () {
console.log('b');
resolve();
}, 0);
});
Promise.all([p1, p2, p3])
.then(function () {
console.log('c');
});
// display:
// a
// b
// c
var p1 = Promise.resolve();
var p2 = new Promise(function (resolve) {
console.log('a');
resolve();
});
var p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('b');
reject();
}, 0);
});
Promise.all([p1, p2, p3])
.then(function () {
console.log('c');
})
.catch(function () {
console.log('ko');
});
// display:
// ko
promise.race
est une méthode prenant en paramètre un itérable de promesses et renvoyant une promesse qui sera résolue ou rejetée dès lors qu’une des promesses de l’itérable sera résolue ou rejetée.
var p1 = new Promise(function (resolve) {
console.log('a');
resolve();
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('b');
resolve();
}, 0);
});
Promise.race([p1, p2])
.then(function () {
console.log('c');
});
// display:
// a
// c
// b
var p1 = new Promise(function (resolve, reject) {
console.log('a');
reject();
});
var p2 = new Promise(function (resolve) {
setTimeout(function () {
console.log('b');
resolve();
}, 0);
});
Promise.race([p1, p2])
.then(function () {
console.log('c');
})
.catch(function () {
console.log('ko');
});
// display:
// a
// ko
// b
Dans le cas suivant, p1 est résolue avant que p2 soit rejetée, la promesse finale est donc résolue car seul le premier résultat est pris en compte.
var p1 = new Promise(function (resolve) {
console.log('a');
resolve();
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('b');
reject();
}, 0);
});
Promise.race([p1, p2])
.then(function () {
console.log('c');
})
.catch(function () {
console.log('ko');
});
// display:
// a
// c
// b
Commentaires