title: JavaScript, promesses et générateurs
slug: javascript-promesses-generateurs
date: 2016-06-17
chapo: Je suis un débutant en JS donc n’hésitez pas à me faire vos retours !

> Conceptually that’s a much cleaner and easier to understand process than what happens when promises are chained multiple times. If you’ve ever tried to understand just how a promise chain is wired up, or explain it to someone else, you’ll know it’s not so simple. I won’t belabor those details here, but I’ve [previously explained it in more detail](https://blog.getify.com/promises-part-5/#the-chains-that-bind-us) if you care to dig into it.
>
> The sync-async pattern above wipes away any of that unnecessary complexity. It also conveniently hides the `then(..)` calling altogether, because truthfully, that part is a wart. The `then(..)` on a promise is an unfortunate but necessary component of the Promise API design that was settled on for JS.
> 
> <cite>*[Promises: All The Wrong Ways](https://blog.getify.com/promises-wrong-ways/)* ([cache](/david/cache/2a774113a3de3e53ca5bf9d45d0b31d1/))</cite>

À la lecture de cet article et utilisant `fetch` avec [son polyfill](https://github.com/github/fetch) sur les [projets Etalab](https://github.com/etalab/), j’ai décidé d’expérimenter ce qui pouvait être fait à base de générateurs pour éviter d’enchaîner les promesses. Ce billet technique sur JavaScript ([encore !](/david/blog/2016/javascript-reduit/)) n’est pas une introduction à [l’objet Promise](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Promise) mais un usage possible, si vous n’avez jamais manipulé ça risque d’être un peu difficile à comprendre. Si vous êtes familier avec les générateurs Python ça peut aider :-).

Prenons un exemple classique où vous voulez récupérer des données depuis une API HTTP :

    :::javascript
    const url = 'http://httpbin.org/get'
    fetch(url)

Jusqu’ici tout va bien mais dès la prochaine ligne ça se complique car il va falloir gérer [les codes d’erreurs à la main](https://github.com/github/fetch#handling-http-error-statuses) :

    :::javascript
    function checkStatus (response) {
      if (response.status >= 200 && response.status < 300) {
        return response
      } else {
        const error = new Error(response.statusText)
        error.response = response
        throw error
      }
    }

Ici on ne retourne la réponse que si elle est en 2XX et sinon on lève notre propre erreur. On peut maintenant enchaîner l’exécution de cette fonction au retour de notre `fetch` :

    :::javascript
    .then(checkStatus)

Puis récupérer la réponse sous forme de JSON :

    :::javascript
    .then((response) => response.json())

Et enfin faire quelque chose d’utile avec :

    :::javascript
    .then((jsonResponse) => {
      // Do something with the jsonResponse.
    })

Si vous en restez là en cas d’erreur vous n’avez pas de moyen de savoir ce qu’il se passe, il ne faut jamais oublier d’ajouter un `catch` à vos chaînes de promesses :

    :::javascript
    .catch(console.error.bind(console))

Ici on fait le minimum en affichant un message dans la console. Investiguer des bugs liés à de l’asynchrone peut rapidement s’avérer être prise de tête si vous ne prenez pas ces précautions. Au même titre, ne mélangez *jamais* différentes implémentations des promises sous peine de devoir re-standardiser ensuite toutes vos promesses via `Promise.resolve(promesse)`…

Donc au final notre promesse chaînée complète ressemble à :

    :::javascript
    fetch(url)
      .then(checkStatus)
      .then((response) => response.json())
      .then((jsonResponse) => {
        // Do something with the jsonResponse.
      })
      .catch(console.error.bind(console))

Ça reste lisible mais c’est [loin d’être idéal](https://www.youtube.com/watch?v=oqwzuiSy9y0), je vous laisse aller lire [l’article en intro](https://blog.getify.com/promises-wrong-ways/) ([cache](/david/cache/2a774113a3de3e53ca5bf9d45d0b31d1/)) pour les différentes raisons.

En utilisant un générateur, je suis arrivé à ce résultat :

    :::javascript
    run(fetchJSON, url)
      .then((jsonResponse) => {
        // Do something with the jsonResponse.
      }, console.error.bind(console))

Qui est finalement la seule logique métier qui m’intéresse. Un seul `.then()` permettant de gérer le `fulfilled`/`rejected` directement en paramètre sans passer par le `.catch()`. Ensuite notre `fetchJSON` est un générateur `*` :

    :::javascript
    function * fetchJSON (url) {
      const rawResponse = yield fetch(url)
      const validResponse = yield checkStatus(rawResponse)
      return validResponse.json()
    }

Proche de la syntaxe Python, on retrouve le mot-clé `yield` qui sert à récupérer les différents éléments de l’itérateur généré. Ici il y a exactement le même traitement que précédemment de manière un peu plus explicite à mon goût. Il reste à définir notre *runner* :

    :::javascript
    function run (generator, ...args) {
      const iterator = generator(...args)
      return Promise.resolve()
        .then(function handleNext (value) {
          const next = iterator.next(value)
          return (function handleResult (next) {
            if (next.done) {
              return next.value
            } else {
              return Promise.resolve(next.value)
                .then(
                  handleNext,
                  (error) => {
                    return Promise.resolve(iterator.throw(error))
                      .then(handleResult)
                  }
                )
            }
          })(next)
        })
    }

Ici c’est la gestion des erreurs qui rend le script un peu velu mais en gros ça suit l’itérateur créé à partir du générateur de manière récursive en retournant des promesses (de promesses (de promesses (etc))). C’est là où se cache maintenant la complexité de vouloir faire de l’asynchrone séquentiel. L’avantage c’est que c’est localisé et réutilisable ensuite pour toutes vos séquences. En attendant [async/await](https://blog.getify.com/not-awaiting-1/) ([cache](/david/cache/bfebf03c42d9609fef758bfc9a305f81/)). Ou [pas](https://blog.getify.com/not-awaiting-2) ([cache](/david/cache/dc98d4cae65338a1be618bab8fa43177/)).

**Le gros point noir dans tout ça, c’est lorsque vous tentez la conversion en ES5 avec Babel.** Vous devez utiliser [babel-polyfill](https://babeljs.io/docs/usage/polyfill/) qui rajoute immédiatement 300Kb à votre joli code… ce qui est envisageable côté serveur ou pour une application complète devient rédhibitoire pour une simple bibliothèque. Même si on ne va au final pas l’utiliser pour cette raison là, ça aura été une incursion intéressante dans l’univers asynchrone de JavaScript en attendant que les fonctionnalités d’ES6/ES7 soient davantage implémentées nativement.

*Je suis un débutant en JS donc n’hésitez pas à me faire vos retours si j’ai raconté des bêtises !*
