Les promises, l’objet venu du futur

Eric Burel
PointJS
Published in
6 min readJul 30, 2019

--

Les bases — Épisode 9

JS, un langage asynchrone

Action Réaction

JavaScript est historiquement un langage conçu pour gérer des événements. Côté client, il s’agit typiquement des actions de l’utilisateur, et côté serveur, des requêtes d’un client.

On utilise alors une logique asynchrone. Un (bon) programme JavaScript ne va jamais se bloquer en attendant un événement. Il va au contraire exécuter tout le code synchrone, jusqu’à ce qu’il n’y ait plus rien d’autre à faire que d’attendre un événement asynchrone.

Sur le plan technique, JavaScript est un langage single-threaded, il n’est donc pas techniquement possible d’exécuter du code en parallèle (en tout cas pas au sens classique du terme).

D’où l’intérêt de répartir au mieux le temps de calcul et d’éliminer tous les temps d’attente, et donc de travailler le plus possible en asynchrone. Cependant, pour nous programmeurs, cela signifie un code un peu plus complexe à lire, car il ne sera pas forcément exécuté dans l’ordre où il est écrit.

La méthode classique pour gérer l’asynchronisme consiste à utiliser des callbacks :

function action1(done){
setTimeout(()=>{
console.log('action 1')
}, 500)
done()
}
function action2(){
console.log('action 2')
}
action1(action2)

Ce programme va afficher immédiatement action 2, sans bloquer, puis ensuite action 1 au bout de 500ms.

Callback hell/pyramid of doom

Les callbacks, c’est très bien, et on pourrait s’en contenter. Mais imaginons que nous n’avons pas juste deux actions à la suite, mais une dizaine.

// pyramid of doom / callback hell
function addAStoneToPyramid(done){
// adding a stone...
if (done) done()
}
function buildPyramidOfDoom(){
addAStoneToPyramid(function(){
console.log("added a stone to the pyramid of doom")
addAStoneToPyramid(function(){
console.log("and another")
addAStoneToPyramid(function(){
console.log("ugh feel tired")
addAStoneToPyramid(function(){
console.log("kill me")
addAstoneToPyramid(function(){
console.log("no wonder pharaohs are all dead")
})
})
})
})
})
}

Les métaphores sont plutôt explicites : vous voyez ici une illustration du callback hell, qui se traduit par l’apparition d’une pyramid of doom (code de plus en plus indenté). On pourrait presque en faire un concert de métal.

En plus de rendre illisible le code, la gestion des erreurs est assez difficile avec cette approche.

Les Promesses

Tenir ses promesses… ou pas

Une promesse, ou promise en anglais, c’est un objet venu du futur qui porte bien son nom. Une promesse “promet” en effet au programme qui la manipule qu’un jour ou l’autre, elle lui fournira une valeur, mais sans que l’on puisse dire quand.

function promiseAStoneToPyramid(){
// le return est appelé immédiatement et ne bloque pas le programme, même si la promise réalise un traitement asynchrone
return new Promise((resolve, reject) => {
const stoneSize = Math.random()*10
if (stoneSize < 5) {
reject(new Error("stone too small"))
}
console.log('added a stone of size', stoneSize)
resolve(stoneSize)
})
}

Au lieu de faire directement ses traitements, la fonction renvoie une promise, qui elle-même prend en entrée une unique callback.

Dès que l’on va appeler promiseAStoneToPyramid(), cette callback est déclenchée et s'exécute jusqu'à ce qu'elle rencontre soit un reject, soit un resolve.

Ces deux fonctions spéciales prennent en paramètre respectivement soit une erreur (reject, la promesse n'est pas tenue), soit une valeur quelconque en cas de succès (résultat d'une requête, d'un calcul lourd, etc.), la promesse est alors résolue (resolve).

Manipuler des promesses, then, catch, finally

La promesse est un objet que l’on va manipuler, afin de définir un comportement à adopter lorsque la promesse est résolue. Ici, dès qu’une pierre et posée, nous allons poser la pierre suivante.

Et voilà ce que donne cette syntaxe :

function throwAwaySmallStones(){
console.log("threw away a small stone")
}

function buildPyramidOfHappiness(){
promiseAStoneToPyramid()
.then(promiseAStoneToPyramid)
.catch(throwAwaySmallStones)

.then(promiseAStoneToPyramid)
.catch(throwAwaySmallStones)

.then(promiseAStoneToPyramid)
.catch(throwAwaySmallStones)

.then(promiseAStoneToPyramid)
.catch(throwAwaySmallStones)

.then(promiseAStoneToPyramid)
.catch(throwAwaySmallStones)
}
buildPyramidOfHappiness()

On peut ajouter une infinité d’étages à notre pyramide, elle sera toujours lisible !

Si l’on veut être sûr de réaliser une action, qu’une erreur se soit produit ou non, vous pouvez ajouter un appel à la fonction finally. On l'utilise par exemple pour fermer une connexion ou un fichier quoi qu'il arrive. Ou pour garder les secrets de la pyramide quoi qu’il arrive.

buildPyramidOfHappiness()
.finally(() => {
console.log('Kill the secret chamber workers')
})

Promesses multiples

Promise.all

Le code précédent est très séquentiel. Cela fait sens pour les suites d’actions, mais parfois, on peut aussi vouloir faire les choses en parallèle, en particulier si l’on souhaite construire une pyramide.

On pourrait tout à fait écrire le code suivant:

promiseAStoneToPyramid()
.then()

promiseAStoneToPyramid()
.then()

Les promesses seraient alors résolues dans un ordre quelconque, voire en parallèle. L’appel à then() déclenche la Promise, et ne fait rien de plus. Le problème est qu’il sera impossible de savoir quand toutes les promesses sont résolues, et donc impossible pour le contre-maître de prendre du repos en attendant que le travail soit fait.

Promess.all nous permet de construire une promesse qui regroupe elle-même un ensemble de promesses.

function buildParallelPyramid(){
return Promise.all([
promiseAStoneToPyramid(),
promiseAStoneToPyramid(),
promiseAStoneToPyramid(),
promiseAStoneToPyramid(),
promiseAStoneToPyramid(),
])
.then('A nice day of pseudo-parallel work!')
.catch(
err => {
console.log('One of the stone was too small :(')
}
)
}
buildParallelPyramid()

Attention, Promise.all, c'est du tout ou rien. Si l'une des promesses est rejetée, la callback du catch sera appelée et toutes les autres promesses seront annulées.

Promise.race, on fait la course ?

Les cas d’usages de Promise.race sont très limités, mais cette fonction fait tout de même partie des spécifications par défaut. Le fonctionnement est assez simple. La promesse globale est résolue dès que l'une des promesses interne est résolue.

function buildStakhanovPyramid(){
return Promise.race([
promiseAStoneToPyramid(),
promiseAStoneToPyramid(),
promiseAStoneToPyramid(),
promiseAStoneToPyramid(),
promiseAStoneToPyramid(),
])
.then(()=>{
console.log("First stone has been posed!")
})
}

Note importante : les appels intermédiaires ne sont pas annulés pour autant, il faut donc éviter les effets de bords dans les promises lorsque l’on utilise Promise.race. Utilisez cette fonction est aussi le signe qu'il vaut peut-être mieux utiliser l'une des librairies présentées dans les liens utiles en bas de cet article.

Autre note : avec Promise.all et Promise.race, le code ne va pas tourner en parallèle, mais plutôt de manière “mélangée”. Pour l’utilisateur final, c’est en tout cas plus rapide que si on lançait les calculs en séquence.

🐍 Syntaxe async/await (ES7)

Le temps passant, vous allez rencontrer de plus en plus souvent une nouvelle syntaxe, async/await. Rien de neuf sur le plan technique, mais cette syntaxe permet d'écrire du code qui s'intègre mieux au reste de l'application.

async function buildAsyncPyramidOfHappiness(){
try {
await promiseAStoneToPyramid()
await promiseAStoneToPyramid()
console.log('Done adding 2 stones')
} catch (err) {
throwAwaySmallStones()
} finally {
console.log('Killing the secret chamber workers...')
return { done: true }
}
}
buildPyramidOfHappiness()

Le mot-clé async est obligatoire et permet de dire au monde entier qu'il s'agit d'une fonction asynchrone. Il rend disponible le mot clé await, qui permet d'appeler une autre fonction asynchrone.

Un plugin Babel vous sera nécessaire tant que cette syntaxe ne sera pas standardisée :

npm install --save-dev @babel/plugin-transform-async-to-generator

Les promesses sont un outil très puissant pour gérer l’asynchronisme. Bien les maîtriser permet de créer des sites dynamiques sans perdre en robustesse.

Liens utiles

  • Bluebird et Q, deux librairies implémentant des promises avec des fonctionnalités supplémentaires pour les usages avancés

--

--

Eric Burel
PointJS

Next.js teacher and course writer. Co-maintainer of the State of JavaScript survey. Follow me to learn new Next.js tricks ✨ - https://nextjspatterns.com/