ES8 est sorti il y a quelques mois et au-delà des quelques changements cosmétiques et des nouvelles APIs, des mots clés ont été introduits dans le langage. Ces nouveaux mots clés cachent aux développeurs l’utilisation de promesses derrière du code qui semble être du code impératif mono thread que l’on a l’habitude d’écrire.

Au départ, Javascript servait à écrire des petits bouts de code à côté d’une page web pour répondre à des évènements ponctuels tels qu’un clique ou un bouton pour valider des champs dans un formulaire. Avec le temps, le code a pris de plus en plus de place dans nos pages web, qui sont devenues de véritables applications.

Dans les pages web modernes, il n’est pas rare d’avoir dans une application Javascript, plusieurs threads qui s’exécutent en parallèle (les WebWorkers) ou qui démarrent des traitements longs dans un serveur.

Pour ne pas rester bloqué… des callbacks

Qu’est-ce qu’on fait quand on a un traitement un peu long et qu’on ne veut pas bloquer notre thread ? Dans d’autres langages de programmation on va souvent créer une thread pour démarrer le traitement et attendre la réponse et utiliser des mécanismes de partage de mémoire ou d’envoi de message pour notifier la thread principale de la fin du traitement.

En Javascript, pendant beaucoup de temps il ne pouvait y avoir qu’une seule thread en exécution (ce qui est encore vrai en NodeJS). Pour palier à cette limitation on fait ce qu’on appelle le multi tâche collaboratif.

Dans les systèmes d’exploitation modernes, on gère des threads multiples en donnant la CPU à chaque thread à tour de rôle ; on fait confiance aux threads pour qu’ils ne monopolisent pas la CPU. C’est ce qu’on appelle le multi tâche préemptif : on interrompt chaque CPU de temps en temps pour le donner à une autre thread.

Dans la cas du multi tâche collaboratif, on fait confiance aux développeurs.

Plus concrètement :

1. On leur demande d’écrire du code qui s’exécute le plus rapidement possible (pour libérer la CPU le plus rapidement possible à d’autres threads),
2. Pour des APIs qui doivent déclencher un traitement long et/ou bloquant, au lieu de bloquer la thread appelante en attente de la fin de l’exécution du traitement, on demande au développeur de définir une fonction qui sera appelée lorsque le traitement sera fini.

Exemple :

function someLongJob(callback) {
    ... // does some long job on another thread, and calls callback() on the calling thread when it is done
}

function myCode() {
    console.log('doing my thing');
    someLongJob(() => {
        console.log("the long job is over, going on");
    });
    console.log('the long job is not necessarily over, but i may release the thred');
}

La fonction someLongJob peut faire des traitements longs/bloquants tels qu’une sauvegarde de fichier (en backend), un appel REST à un service externe, ou un appel au service de géolocalisation. Ce qui est important est que la thread du code Javascript ne reste jamais bloquée.

Callback hell

Les callbacks, ça fonctionne très bien pour des cas simples. Mais dans les applications modernes, il arrive très souvent de devoir enchaîner plusieurs traitements longs. Dans une « vraie » application, il faut aussi pouvoir récupérer des sorties des traitements déclenchés, détecter et traiter les cas d’erreur.

Exemple :

    
    console.log('doing my thing');
    someLongJob((success, error) => {
        console.log("the long job is over, going on");
        if (error) {
            console.error(error);
        } else {
            console.log("success", success);
            someOtherLongJob(() => {
                if (error) {
                    console.error(error);
                } else {
                    console.log("success", success);
                    console.log("ok, second job is over");
                }
            });
        }
    });
    console.log('the long job is not necessarily over, but i may release the thred');

Dans cet exemple, on utilise les variables success et erreur pour passer des résultats et erreurs à l’appelant. On voit qu’on a dû écrire beaucoup plus de codes juste pour traiter deux appels successifs…

Promises

Pour simplifier ça, le concept de promises a été crée. Le paradigme derrière est légèrement différent. Au lieu de passer un callback à une fonction qui démarre un traitement long, la fonction nos retourne un objet: une promesse (Promise). Cet objet représente le traitement asynchrone qui s’exécute possiblement en parallèle.

Ces objets viennent avec une API très utile. On y trouvera des fonctions comme then et catch qui permettent un enchaînement de traitements et un traitement d’erreurs simplifié. On y trouvera aussi la méthode Promise.all qui permet le lancement d’un ensemble de traitements concurrents, et l’obtention d’une seule promesse sur un ensemble de traitements.

Exemple:

    console.log('doing my thing');
    someLongJob2()
        .then((value) => {
            console.log("success", value);
            return someOtherLongJob2();
        })
        .then((value) => {
            console.log("success", value);
            console.log("ok, second job is over");
        })
        .catch((error) => {
            console.error(error);
        });
    console.log('the long job is not necessarily over, but i may release the thred');

Dans cet exemple, on voit que le code s’approche plus de l’ordre d’exécution des traitements, et qu’il nous permet de gérer tous les cas d’erreur dans un seul endroit.

Cool… but… On écrit encore beaucoup trop de code !

Dans ES8, on a simplifié encore plus la vie des développeurs qui aimeraient bien écrire du code qui s’approche le plus possible du code synchrone et séquentiel et que le compilateur/interpréteur gère la complexité des promesses.

C’est pour cela que deux mots clés ont été introduits en ES8 :

1. await X, ou X : est une fonction qui retourne une promesse.

Le mot clé await est purement du sucre syntaxique.

try {
	let x = await f();
	g(x);
} catch(error) {
	c(error);
}

est équivalent à

let X = undefined;
f()
.then((value) => {
	x = value;
	return g(x);
})
.catch((error) => {
	c(error);
});

Cela nous permet d’écrire du code pour récupérer le résultat d’un traitement (l’appel à f()), le passer à g(), et gérer les cas
d’erreur comme on a l’habitude de faire en Javascript.

2. async function : est une fonction qui retourne une promesse et qui utilise des await ;), donc qui retourne une promesse.

Exemple :

async function h() {
    try {
      let x = await f();
      return g(x);
    } catch(error) {
      c(error);
    }
}

Là aussi on a du sucre syntaxique. On cache au développeur qu’en appellant h() on obtient une promesse, pas une valeur.

Le code est donc équivalent à :

function h() {
    return f()
        .then(x => {
            return g(x);
        })
        .catch(error => {
            c(error);
        });
}

Dans les deux cas, on peut appeler h() de la façon suivante :

h().then(value => console.log("final value", value));

On peut aussi utiliser le mot clé await pour cacher l’utilisation d’une promesse :

console.log("final value", await h());

Pour aller plus loin sur ES8 :

Dans cet article, on a vu deux nouveaux mots clés en Javascript ES8 : async et await. Ils sont utilisés dans du code qui déclenche et coordonne un des traitements concurrents et bloquants. Cela nous permet d’écrire du code qui s’exécutera de façon asynchrone de façon presque impérative/séquentielle.

Pour aller plus loin, je vous recommande de lire les docs Mozilla sur les fonctions asynchrones et sur les promesses.

Le site http://callbackhell.com/ présente aussi une bonne discussion sur les callbacks, le callback hell et comment l’éviter. Tout ça sans utiliser les promesses ni les fonctions asynchrones !