Activité Programmation en JavaScript : fonctions asynchrones et promesses

Situation

Vous êtes au début d’un projet de développement d’une application web. Vous envisagez d’utiliser JavaScript pour le développement de la partie client et de la partie serveur. Vous êtes familiarisé avec le langage JavaScript et avec la programmation asynchrone pour le traitement d’événements en provenance de dispositifs d’entrées/sorties ou de worker threads. Les notions de file d’attente d’événements (event queue), de boucle d’événements (event loop), de fonction asynchrone et de fonction callback vous sont connues. Cependant, vous avez remarqué qu’avec les fonctions callback, les appels imbriqués de fonctions asynchrones rendent le code difficile à lire (callback hell) et les erreurs difficiles à traiter. En parlant de cela autour de vous, vous apprenez que l’utilisation de promesses constitue une solution à ce problème. Vous décidez donc de consacrer un peu de temps à leur étude.

Consigne

Réaliser les labs décrits ci-après et consigner le résultat de votre travail dans un document. Lisez attentivement tout le texte. Le but est d’apprendre JavaScript en exécutant du code, pas d’exécuter du code bêtement. Le travail est individuel, car il est important que tous les membres du groupe de projet soient capables de programmer la solution.

Un rapport qui ne contient que les réponses aux questions explicitement posées n’est pas suffisant. Le résultat de ce travail fait partie de la livraison du sprint 0.

Objectifs

À la fin de ce travail, vous devez :

  1. Connaître la notion de programmation asynchrone.
  2. Connaître la notion de promesse (promise).
  3. Connaître le rapport entre les promesses et la syntaxe async/await.
  4. Connaître les avantages de la programmation asynchrone.
  5. Connaître les avantages des promesses par rapport aux fonction callback.
  6. Connaître les avantages qu’apporte la syntaxe async/await pour l’utilisation des promesses.

Résultat attendu

  • Un bref rapport qui décrit votre travail et votre démarche.

Ressources

Logiciel :

  • Un éditeur de texte (VS Code).
  • Le gestionnaire de dépôt git.
  • La plateforme Node.js (node, npm, npx, etc.)
  • Un shell (bash, powershell, cmd).
  • Un navigateur web (Chrome, Firefox, IE).

L’enfer des callback (lab7)

Dans la première partie de ce travail, nous revenons d’abord rapidement sur la notion de séquence d’instructions qui est à la base de la programmation impérative. Nous revenons ensuite sur la manière d’exécuter une séquence d’opérations asynchrone en utilisant des fonction callback pour mettre en évidence quelques difficultés liées à leur utilisation.

Mise en place de l’environnement

Cette activité fait suite à l’activité À la découverte de JavaScript. Assurez-vous d’avoir effectué la mise en place de votre environnement conformément au point 6.

Rendez-vous dans votre répertoire de travail et lancer les commandes ci-après pour récupérer le projet lab7, installer les dépendances et ouvrir le projet dans Visual Studio Code.

1
2
3
4
git clone https://gitlab.epai-ict.ch/m120/lab7.git
cd lab7
npm install
code .

Exécuter des fonctions synchrone en séquence

Le principe de base de la programmation impérative est d’exécuter une séquence d’instructions. Dans les langages de programmation impératifs, une séquence d’instructions doit donc être exprimée de la manière la plus simple possible, généralement en écrivant les instructions dans l’ordre, les unes après les autres. Lorsqu’une instruction est un appel de sous‑programme (procédure ou fonction), le fil d’exécution (thread) exécute les instructions du sous‑programme avant de passer à l’instruction suivante.

Copiez le code ci-après dans le fichier main.js et lancez l’exécution. Organisez votre écran de manière à voir le navigateur et l’éditeur de code l’un à côté côté de l’autre et ouvrez les outils de développement du navigateur.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInSequence = function () {
  let result

  console.log('Appel de la fonction one')
  result = oneTwoThreeLib.one(1, 2)
  console.log('Résultat de la fonction one : ', result)

  console.log('Appel de la fonction two')
  result = oneTwoThreeLib.two(result, 4)
  console.log('Résultat de la fonction two : ', result)

  console.log('Appel de la fonction three')
  result = oneTwoThreeLib.three(result, '5')
  console.log('Résultat de la fonction three : ', result)
}

console.log('Début de l’appel des trois fonctions synchrones.')
doSomethingInSequence()
console.log('Fin de l’appel des trois fonctions synchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')

Dans la console, vous pouvez voir que toutes les instructions du programme ont été exécutées en séquence. Les deux éléments <script> du fichier index.html ont également été traités l’un après l’autre. Le premier (one-two-three-lib.js) contient une seule instruction d’affectation dont l’effet est de créer un objet qui représente un module (nous reviendrons sur ce point dans une prochaine activité). Cet objet dispose de six méthodes. Les trois méthodes one, two et three que nous venons d’utiliser et trois méthodes oneAsync, twoAsync et threeAsync qui font exactement la même chose que les premières, mais de manière asynchrone. La méthode one s’exécute en 3s, la méthode two s’exécute en 1s et la méthode three s’exécute en 2s.

Exécuter des fonctions asynchrones en parallèle

Voyons maintenant ce qu’il se passe si notre un programme se compose de trois appels de fonctions asynchrones l’une à la suite de l’autre.

Remplacer le code du fichier main.js par le code ci-dessous et enregistrer les modifications (la page devrait se recharger avec le live-reload, rafraîchissez la page si ce n’est pas le cas.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInParallelAsync = function () {
  console.log('Appel de la fonction oneAsync')
  oneTwoThreeLib.oneAsync(1, 2,
    function (result) {
      console.log('Résultat de oneAsync : ', result)
    })

  console.log('Appel de la fonction twoAsync')
  oneTwoThreeLib.twoAsync(3, 4,
    function (result) {
      console.log('Résultat de twoAsync : ', result)
    })

  console.log('Appel de la fonction threeAsync')
  oneTwoThreeLib.threeAsync(4, 5,
    function (result) {
      console.log('Résultat de threeAsync : ', result)
    })
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInParallelAsync()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')

Dans la console du navigateur, vous pouvez constater que les trois fonctions sont bien appelées en séquence, mais elle retournent immédiatement après avoir lancé l’opération asynchrone, sans attendre le résultat de ces opérations. Lorsqu’une opération asynchrone se termine, elle le signale en plaçant un événement dans la file d’attente d’événements. Lorsque la boucle d’événement du programme traite cet événement, elle appelle la fonction callback correspondante.

De plus, comme on peut le voir dans la trace, l’ordre dans lequel sont appelées les fonctions callback ne correspond pas à l’ordre dans lequel les fonctions asynchrones ont été appelées. Cet ordre dépend du temps d’exécution des opérations asynchrones.

En programmation asynchrone, il importe de distinguer l’appel et l’exécution d’une fonction. Si on appelle en séquence plusieurs fonctions asynchrones (non-bloquantes), ces fonctions sont exécutées en parallèle, et non l’une après l’autre comme c’est le cas avec des fonctions synchrones (bloquantes).

L’exécution d’opérations asynchrones en parallèle peut être intéressante lorsque l’on a besoin de plusieurs résultats indépendants l’un de l’autre pour effectuer un traitement. On peut signaler ici une première difficulté liée à l’utilisation de fonctions callback. S’il est facile de traiter le résultat de chaque opération individuellement, il est beaucoup plus difficile d’effectuer un traitement qui nécessite les résultats de toutes les opérations.

Exécuter des fonctions asynchrones en séquence

Voyons maintenant comment exécuter des fonctions asynchrones en séquence. Pour cela, nous devons appeler la deuxième fonction lorsque l’on reçoit le résultat de la première et pas avant. De même, nous devons appeler la troisième, seulement lorsque l’on reçoit le résultat de deuxième. La solution consiste donc à appeler la deuxième fonction dans la fonction callback de la première et la troisième dans la fonction callback de la deuxième.

Copiez le code ci-après dans le fichier main.js, exécutez-le et étudiez la trace du programme dans la console du navigateur.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInSequenceAsync = function () {
  console.log('Appel de la fonction oneAsync')
  oneTwoThreeLib.oneAsync(1, 2,
    function (result) {
      console.log('Résultat de oneAsync : ', result)

      console.log('Appel de la fonction twoAsync')
      oneTwoThreeLib.twoAsync(result, 4,
        function (result) {
          console.log('Résultat de twoAsync : ', result)

          console.log('Appel de la fonction threeAsync')
          oneTwoThreeLib.threeAsync(result, 5,
            function (result) {
              console.log('Résultat de threeAsync : ', result)
            })
        })
    })
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInSequenceAsync()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')

Cette manière d’écrire comporte un certain nombre d’inconvénients. L’un des plus importants est l’augmentation de la complexité cyclomatique par rapport à l’utilisation de fonctions synchrones. Un autre inconvénient est le traitement des erreurs, beaucoup plus difficile à réaliser correctement.

La promesse d’un monde meilleur (lab7 suite)

Dans la seconde partie de ce travail, nous revenons sur le pourquoi de la programmation asynchrone et sur les difficultés liées à l’utilisation de fonctions callback. Nous abordons ensuite la notion de promesse et la manière dont celle-ci facilite la programmation asynchrone. Pour finir, nous nous intéressons aux mots-clés async et await qui permettent désormais à plusieurs de langages de programmation, dont JavaScript, de supporter les promesses, faisant de la programmation asynchrone un paradigme à part entière.

Pourquoi avons-nous besoin de fonctions asynchrones ?

Avant de poursuivre, interrogeons-nous sur l’utilité de ces fonctions asynchrone. Pourquoi ne peut-on pas se contenter de fonctions synchrones ? La programmation asynchrone est rendue nécessaire par le besoin d’avoir des programmes à la fois réactifs et robustes.

La réactivité est importante pour les interfaces utilisateur graphiques (GUI). En effet, une fenêtre qui se fige lorsque l’utilisateur clique sur un bouton n’est pas un gage de qualité. Elle est également importante du côté serveur où l’on cherche à maximiser le nombre de requêtes pouvant être traitées par un seul serveur.

Une manière d’atteindre cet objectif consiste à utiliser plusieurs fils d’exécution (thread). Cela permet l’exécution de plusieurs tâches en parallèle. Toutefois, la programmation parallèle est extrêmement difficile. Les erreurs sont donc fréquentes. L’utilisation d’un seul thread va à l’encontre de l’objectif de réactivité et l’utilisation de plusieurs threads va donc à l’encontre celui de robustesse. Que faire ? La programmation asynchrone se propose de réconcilier ces deux objectifs.

Les principales difficultés de la programmation parallèle sont liées à la synchronisation des threads lors de l’accès à des ressources partagées, tout particulièrement, lors de l’accès à la mémoire virtuelle. La programmation asynchrone propose de respecter deux principes pour éviter ce problème.

  1. Le premier va dans le sens de l’objectif de robustesse. Le programmeur réalise son programme avec un seul thread et s’interdit de créer lui-même des threads supplémentaires.
  2. Le second va dans le sens de l’objectif de réactivité. Le programmeur utilise des bibliothèques qui mettent en oeuvre des threads pour réaliser des d’E/S asynchrones ou des worker threads. Toutefois, la communication entre ces threads et le thread du programme se fait exclusivement par l’intermédiaire d’une file d’attente d’événements (messages).

Le second principe implique que le programme doit être construit autour d’une boucle d’événement (event loop). Celle-ci peut être réalisée par le programmeur, mais le plus souvent elle est implémentée dans un framework (Framework .NET, Web API des navigateurs, etc.). Lorsque c’est le cas, il est nécessaire de pouvoir la personnaliser avec les gestionnaires d’événements du programme. On peut réaliser cela avec des fonctions callback. Mais, nous l’avons vu, celles-ci s’accompagne d’un certain nombre de difficultés qui vont à l’encontre de l’objectif de robustesse.

Une meilleures solution est d’utiliser des promesses. En effet, une fonction callback sert à recevoir le résultat lorsque l’opération asynchrone est terminée. Au lieu de lui passer une fonction callback, pourquoi ne pas demander à la fonction asynchrone de nous renvoyer un objet qui nous permettra d’obtenir le résultat lorsqu’il sera disponible ? Un tel objet est précisément ce qu’on appelle communément une promesse.

Qu’est-ce qu’une promesse ?

Une promesse (promise) est un objet qui encapsule un appel de fonction asynchrone. Elle représente la promesse, dans le sens courant du terme, faite par la fonction asynchrone de fournir un résultat. Ce résultat n’est peut-être pas encore calculé, mais il sera ultérieurement, à moins qu’une erreur se produise. Si l’opération asynchrone se termine sans erreur, on dit que la promesse est tenue ou résolue (resolved). Dans le cas contraire, on dit que la promesse est rompue ou rejetée (rejected).

Il n’est plus nécessaire d’attacher un gestionnaire d’événement lors de l’appel d’une fonction asynchrone. Celle-ci renvoie immédiatement une promesse que l’on peut manipuler comme bon nous semble. Pour recevoir le résultat, il suffit d’utiliser la méthode then de la promesse, à n’importe quel moment, pour lui attacher une fonction callback. Cette dernière est appelée aussitôt que le résultat de l’opération asynchrone est disponible.

Le code ci-dessous montre deux appels équivalents de la fonction onAsync en lui passant une fonction callback d’abord, puis en utilisant la promesse qu’elle renvoie lorsqu’on l’appelle sans fonction callback.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Appelle la fonction oneAsync avec une fonction
// callback pour recevoir le résultat.
oneTwoThreeLib.oneAsync(1, 2, function (result) {
  // Le paramètre result contient le résultat de
  // l’opération asynchrone.
  console.log('Résultat de oneAsync (1) : ', result)
})

// Appelle la fonction oneAsync et affecte la
// promesse qu’elle renvoie à la variable p1.
const p1 = oneTwoThreeLib.oneAsync(1, 2)

// ... faire quelque chose si nécessaire ...

// Attache une fonction callback à la promesse
// pour reçevoir le résultat.
p1.then(function (result) {
  // Le paramètre result contient le résultat de
  // l’opération asynchrone.
  console.log('Résultat de oneAsync (2) : ', result)
})

// ... faire autre chose si nécessaire

// Attache une nouvelle fonction callback à la
// promesse pour reçevoir le résultat.
p1.then(function (result) {
  // Le paramètre result contient le résultat de
  // l’opération asynchrone.
  console.log('Résultat de oneAsync (3) : ', result)
})

Si l’on reste là, les promesses ne sont pas très différentes des fonctions callback. Voyons un à un les avantages des promesses et commençons par l’exécution de fonction asynchrone en séquence (l’une à la suite de l’autre).

Exécuter de fonctions asynchrones en séquence avec des promesses

La méthode then est elle-même une méthode asynchrone. Elle associe une fonction callback à l’opération asynchrone et renvoie immédiatement une promesse. La promesse renvoyée par la fonction then encapsule le résultat de la fonction callback. Cela permet d’enchaîner les appels sans augmenter la complexité cyclomatique (voir plus haut).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Appelle la fonction onAsync et affecte la promesse
// renvoyée à la variable p1.
const p1 = oneTwoThreeLib.oneAsync(1, 2)

// Attache une fonction une callback à l’aide de la
// méthode then et affecte la promesse renvoyée à la
// variable p2.
const p2 = p1.then(function (result) {
  // L’argument est le résultat de oneAsync.
  console.log('Résultat de oneAsync : ', result)

  // Appel la fonction twoAsync et renvoie son
  // résultat (ici sous la forme d’une promesse).
  return oneTwoThreeLib.twoAsync(result, 4)
})

// Attache une fonction une callback à l’aide de la
// méthode then de p2 qui est appelé dès que le résultat
// de la fonction callback est disponible (ici, celui de
// l’opération asynchrone de twoAsync).
const p3 = p2.then(function (result) {
  // L’argument est le résultat renvoyé par la
  // fonction callback attachée à p1 (ici le résultat
  // de l’opération asynchrone de twoAsync).
  console.log('Résultat de twoAsync : ', result)

  // Renvoie une valeur qui n’est pas une promesse.
  // la fonction callback attachée avec then peut
  // renvoyer n’importe quelle valeur.
  return 42
})

const p4 = p3.then(function (result) {
  // L’argument est le résultat renvoyé par la
  // fonction callback attachée à p2 (ici, 42).
  console.log('Résultat : ', result)

  // Ne renvoie rien. Il n’est pas nécessaire de
  // renvoyer une valeur. L’argument result d’une
  // callback attachée à p4 est undefined.
})

p4.then(function (result) {
  // L’argument est le résultat renvoyé par la
  // fonction callback attachée à p2 (ici, undefined).
  console.log('Résultat : ', result)
})

Cet exemple montre que l’on peut enchaîner indéfiniment les appels de fonction asynchrone sans augmenter la complexité cyclomatique. De plus, la méthode then renvoie une promesse dans tous les cas. Même lorsque la fonction callback n’est pas elle-même asynchrone ou ne renvoie rien du tout (voir les fonction callback de p2 et p3).

En pratique, il n’est pas nécessaire d’affecter la promesse renvoyée par then à une variable si on ne l’utilise que pour chaîner des appels. La méthode then peut être appelée sur n’importe quelle expression de type Promise. On peut donc appeler cette méthode sur un appel à une fonction qui renvoie une promesse. Dans notre code, c’est le cas de la méthode then elle-même et de la fonction oneAsync.

On peut donc réécrire le code précédent de la manière suivante :

1
2
3
4
5
6
7
8
9
10
11
oneTwoThreeLib.oneAsync(1, 2).then(function (result) {
  console.log('Résultat de oneAsync : ', result)
  return oneTwoThreeLib.twoAsync(result, 4)
}).then(function (result) {
  console.log('Résultat de twoAsync : ', result)
  return 42
}).then(function (result) {
  console.log('Résultat : ', result)
}).then(function (result) {
  console.log('Résultat : ', result)
})

Cette manière d’écrire met en évidence le fait que la méthode then est appliquée à l’appel d’une fonction asynchrone, mais n’est pas des plus élégantes. La forme utilisée dans la fonction doSomethingInSequence du code ci-après est généralement préférée. Copiez ce code dans le fichier main.s, exécutez-le et étudiez attentivement la trace qui s’affiche dans la console du navigateur.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInSequenceAsync = function () {
  console.log('Appel de la fonction oneAsync')
  oneTwoThreeLib.oneAsync(1, 2)
    .then(function (result) {
      console.log('Résultat de oneAsync : ', result)

      console.log('Appel de la fonction twoAsync')
      return oneTwoThreeLib.twoAsync(3, 4)
    })
    .then(function (result) {
      console.log('Résultat de twoAsync : ', result)

      console.log('Appel de la fonction threeAsync')
      return oneTwoThreeLib.threeAsync(4, 5)
    })
    .then(function (result) {
      console.log('Résultat de threeAsync : ', result)
    })
}

console.log('Début de l’appel des trois fonctions asynchrones en séquence.')
doSomethingInSequenceAsync()
console.log('Fin de l’appel des trois fonctions asynchrones en séquence.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')

Nous avons vu que l’utilisation de promesse nous permet d’enchaîner facilement des appels de fonctions asynchrones de manière à les exécuter en séquence (l’une après l’autre). Qu’en est-il de l’exécution de fonction asynchrone en parallèle ?

Exécuter de fonctions asynchrones en parallèle avec des promesses

Comme nous l’avons déjà mentionné, il peut être intéressant d’exécuter des opérations asynchrones en parallèle. En effet, si l’on a besoin des résultats de plusieurs requêtes HTTP ou de plusieurs requêtes SQL pour effectuer un traitement, il n’est pas nécessaire d’attendre que l’une soit terminée pour lancer la suivante.

Le code suivant appelle les trois fonctions asynchrones en séquence. L’exécution des opérations asynchrone s’effectue donc en parallèle. Nous utilisons la promesse renvoyée par chacune d’elle pour attacher une fonction callback.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInParallelAsync = function () {
  console.log('Appel de la fonction oneAsync')
  const p1 = oneTwoThreeLib.oneAsync(1, 2)
  p1.then(function (result) {
    console.log('Résultat de oneAsync : ', result)
  })

  console.log('Appel de la fonction twoAsync')
  const p2 = oneTwoThreeLib.twoAsync(3, 4)
  p2.then(function (result) {
    console.log('Résultat de twoAsync : ', result)
  })

  console.log('Appel de la fonction threeAsync')
  const p3 = oneTwoThreeLib.threeAsync(4, 5)
  p3.then(function (result) {
    console.log('Résultat de threeAsync : ', result)
  })
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInParallelAsync()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')

Cette fois encore, si nous en restons là, les promesses ne sont pas différentes des fonctions callback. Toutefois, nous avions signalé que l’une des difficultés liées à l’utilisation de fonctions callback était de recevoir dans une unique fonction callback, les résultats de plusieurs opérations asynchrones. Les promesses rendent cela extrêmement simple.

L’objet prédéfini Promise dispose d’une méthode all qui reçoit en paramètre un tableau de promesses et qui renvoie une nouvelle promesse. Cette nouvelle promesse est tenue lorsque toutes les promesses sont tenues. Les résultats des différentes opérations sont fournis dans un tableau et l’ordre des résultats correspond à l’ordre des promesses passées à la méthode all. Copiez le code suivant dans le fichier main.js et exécutez-le pour voir cette méthode en action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInParallelAsync = function () {
  console.log('Appel de la fonction oneAsync')
  const p1 = oneTwoThreeLib.oneAsync(1, 2)

  console.log('Appel de la fonction twoAsync')
  const p2 = oneTwoThreeLib.twoAsync(3, 4)

  console.log('Appel de la fonction threeAsync')
  const p3 = oneTwoThreeLib.threeAsync(4, 5)

  // Crée une promesse qui ne sera tenu que lorsque les
  // promesse p1, p2 et p3 aurons toute été tenues. Le
  // paramètre de all est un tableau de promesses.
  const p4 = Promise.all([p1, p2, p3])

  p4.then(function (result) {
    console.log('Toutes promesses ont été tenue!')

    // Le paramètre result contient un tableau dont chaque
    // élément est le résultat d'une des promesses. L'ordre
    // des résultats dans le tableau correspond à l'ordre des
    // promesses dans le tableau passé à la méthode all.
    console.log('- Résultat de oneAsync : ', result[0])
    console.log('- Résultat de twoAsync : ', result[1])
    console.log('- Résultat de threeAsync : ', result[2])
  })
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInParallelAsync()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')

Détecter et traiter les exceptions

Une autre difficulté de la programmation asynchrone à l’aide de fonction callback est le traitement d’erreur. Traiter les erreurs est toujours difficile, mais les mécanismes de traitement d’exception structurés des (try-catch) des langages modernes simplifient grandement cette tâche.

Le code ci-après génère une exception en passant à la fonction one un argument de type string alors que celle-ci exige des arguments de type number. On peut jeter une exception n’importe où dans le code à l’aide de l’instruction throw.

Si une exception est jetée durant l’exécution d’un bloc try, la séquence normale est interrompue et le code se poursuit au début du bloc catch correspondant. Le bloc try permet de détecter l’exception et le bloc catch de la traiter. Si une exception survient et qu’il n’y a pas de bloc try pour la détecter, on dit que l’exception n’est pas gérée (unhandled exception). Une exception non gérée provoque le plus souvent un affichage de l’erreur dans la console et l’arrêt du programme.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInSequence = function () {
  let result

  try {
    console.log('Appel de la fonction one')
    // Génère une exception à cause du type du paramètre.
    result = oneTwoThreeLib.one('1', 2)
    console.log('Résultat de la fonction one : ', result)

    console.log('Appel de la fonction two')
    result = oneTwoThreeLib.two(result, 4)
    console.log('Résultat de la fonction two : ', result)

    console.log('Appel de la fonction three')
    result = oneTwoThreeLib.three(result, '5')
    console.log('Résultat de la fonction three : ', result)
  } catch (err) {
    console.log('Une erreur est survenue : ', err)
  }
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInSequence()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')

Avec des fonctions asynchrones, ce principe n’est plus applicable. En effet, il est possible que des exceptions se produisent alors que l’exécution du bloc try est déjà terminée depuis longtemps. Les erreurs produites par une fonction asynchrone doivent donc être traitées dans la fonction callback, ou dans une fonction callback dédiée à la gestion des erreurs. Toutefois, la manière de traiter et de propager les exceptions n’est pas clairement définie et est laissée à la discrétion du programmeur.

L’utilisation de promesse facilite la gestion des exceptions grâce à la méthode catch. Grâce à cette méthode, on peut attacher une fonction callback sur la dernière promesse d’une chaîne et avoir ainsi un équivalent du bloc catch correspondant à un bloc try. Le bloc try étant ici la chaîne de promesses. Si une exception se produit n’importe où dans la chaîne de callback, la fonction callback attachée avec la méthode catch est aussitôt appelée. La promesse est rompue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInSequenceAsync = async function () {
  console.log('Appel de la fonction oneAsync')
  // Génère une exception à cause du type du paramètre.
  oneTwoThreeLib.oneAsync('1', 2)
    .then(function (result) {
      console.log('Résultat de oneAsync : ', result)

      console.log('Appel de la fonction twoAsync')
      return oneTwoThreeLib.twoAsync(3, 4)
    })
    .then(function (result) {
      console.log('Résultat de twoAsync : ', result)

      console.log('Appel de la fonction threeAsync')
      return oneTwoThreeLib.threeAsync(4, 5)
    })
    .then(function (result) {
      console.log('Résultat de threeAsync : ', result)
    })
    .catch(function (reason) {
      console.log('Une erreur est survenue : ', reason)
    })
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInSequenceAsync()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')

La promesse de la méthode all est tenue si et seulement si toutes les promesses sont tenues. Si l’une des promesses est rompue, alors celle de la méthode all l’est aussi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInParallelAsync = function () {
  console.log('Appel de la fonction oneAsync')
  const p1 = oneTwoThreeLib.oneAsync('1', 2)

  console.log('Appel de la fonction twoAsync')
  const p2 = oneTwoThreeLib.twoAsync(3, 4)

  console.log('Appel de la fonction threeAsync')
  const p3 = oneTwoThreeLib.threeAsync(4, 5)

  Promise.all([p1, p2, p3])
    .then(function (result) {
      console.log('Résultat de oneAsync : ', result[0])
      console.log('Résultat de twoAsync : ', result[1])
      console.log('Résultat de threeAsync : ', result[2])
    })
    .catch(function (reason) {
      console.log('Une erreur est survenue!')
    })
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInParallelAsync()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')

Support des promesse par le lanage de programmation (async/await)

L’utilisation de promesses facilite la programmation asynchrone. Toutefois, la manière dont nous avons utilisé les promesses jusqu’ici ne fait appel qu’à des constructions syntaxiques aujourd’hui typiques des langages de programmation orientés objets : création d’objets, appel de méthode et expressions lambda.

Pour devenir un paradigme de programmation à part entière (comme la programmation structurée ou la programmation orientée objet), la programmation asynchrone a besoin d’être supportée par les langages de programmation. Ce support prend la forme de nouvelles construction syntaxique avec les mots clés async et await et fait des promesses un élément du langage à part entière. Ces mots clés sont notamment supporté par C#, Python et EcmaScript 2017 (JavaScript 8) . Cette syntaxe est supportée par la dernière version de tous les navigateurs à l’exception de IE. Plusieurs autres langages supportent une syntaxe similaire, notamment, C# et Python.

En utilisant ces constructions syntaxiques, notre fonction doSomethingInSequenceAsync() ne diffère pratiquement plus de la fonction doSomethingInSequence() du début.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
console.log('>>> Début de l’exécution de l’élément script (main.js).')

// Le mot clé await ne peut être utilisé que dans une fonction
// asynchrone. On indique qu'une fonction est asynchrone avec le
// mot clé async.
const doSomethingInSequenceAsync = async function () {
  let result

  try {
    console.log('Appel de la fonction oneAsync')
    result = await oneTwoThreeLib.oneAsync(1, 2)
    console.log('Résultat de oneAsync : ', result)

    console.log('Appel de la fonction twoAsync')
    result = await oneTwoThreeLib.twoAsync(result, 4)
    console.log('Résultat de twoAsync : ', result)

    console.log('Appel de la fonction threeAsync')
    result = await oneTwoThreeLib.threeAsync(result, 5)
    console.log('Résultat de threeAsync : ', result)
  } catch (err) {
    console.log('Une erreur est survenue : ', err)
  }
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInSequenceAsync()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
console.log('>>> Début de l’exécution de l’élément script (main.js).')

const doSomethingInParallelAsync = async function () {
  let result

  try {
    console.log('Appel de la fonction oneAsync')
    const p1 = oneTwoThreeLib.oneAsync(1, 2)

    console.log('Appel de la fonction twoAsync')
    const p2 = oneTwoThreeLib.twoAsync(3, 4)

    console.log('Appel de la fonction threeAsync')
    const p3 = oneTwoThreeLib.threeAsync(4, 5)

    result = await Promise.all([p1, p2, p3])

    console.log('Résultat de oneAsync : ', result[0])
    console.log('Résultat de twoAsync : ', result[1])
    console.log('Résultat de threeAsync : ', result[2])
  } catch (err) {
    console.log('Une erreur est survenue : ', err)
  }
}

console.log('Début de l’appel des trois fonctions asynchrones.')
doSomethingInParallelAsync()
console.log('Fin de l’appel des trois fonctions asynchrones.')

console.log('>>> Fin de l’exécution de l’élément script (main.js).')
console.log('\n\n')

Conclusion

La programmation asynchrone est désormais incontournable. Pour la réalisation de GUI bien sûr, mais aussi pour la réalisation de serveurs. Dans ce contexte, une promesse est un objet qui permet d’encapsuler un appel de fonction asynchrone. L’utilisation de promesses, qui peuvent être facilement combinées, et d’une plateforme d’exécution appropriée (navigateur, Node.js, Framework .NET, JVM, Python, etc.) facilite grandement la réalisation de programme asynchrone. De plus, bon nombre de langage supporte aujourd’hui les mots clés async et await qui font des promesses une partie intégrante de ces langages.