Skip to content

11. Programmation événementielle et fonctions asynchrones

Une fonction asynchrone est une fonction dont l'exécution est lancée mais dont on n'attend pas immédiatement le résultat. Lorsque l'exécution est terminée, la fonction asynchrone signale son résultat — par un événement, ou par une [Promise], comme on le verra plus loin.

Ce mode de fonctionnement est bien adapté à l'exécution dans un navigateur, où l'application réagit en permanence à des événements (clics, frappe clavier, réponses réseau...). Les scripts de ce chapitre sont exécutés par [node.js], qui utilise lui aussi une boucle d'événements :

  • l'exécution du code principal du script est le 1er événement traité ;
  • si ce code a lancé des tâches asynchrones, l'exécution du script se poursuit tant qu'elles ne sont pas terminées — elles émettent un événement une fois terminées, mis en file d'attente dans la boucle d'événements ;
  • le script principal doit s'abonner à ces événements pour récupérer leurs résultats ;
  • le script ne se termine que lorsque tous les événements qu'il a émis ont été traités.

Les scripts de ce chapitre se trouvent dans le dossier [async] du projet.

11.1. script [async-01]

Ce script montre le comportement d'un script comportant une action asynchrone, à l'aide de la fonction native [setTimeout] :

'use strict';

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure());

// setTimeout arme un timer de 1000 ms (2ième paramètre) et retourne immédiatement le n° de ce timer
// lorsque le timer a épuisé les 1000 ms il émet un événement qui est mis en file d'attente du runtime
// lorsque l'événement est traité par le runtime, la fonction (1er paramètre) est exécutée
setTimeout(function () {
  // ce code sera exécuté lorsque le timer aura atteint la valeur 0
  console.log("[fin de l'action asynchrone setTimeout],", heure(débutScript));
}, 1000)

// s'affichera avant le msg de la fonction interne au timer
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire d'affichage heure et durée
function heure(début?: moment.Moment): string {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // faut-il calculer une durée ?
  if (début) {
    const durée = now.valueOf() - début.valueOf();
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage heure + durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}
  • lignes 14-17 : [setTimeout] admet deux paramètres : une fonction à exécuter, et un délai en millisecondes. Son exécution est instantanée : elle arme un minuteur et rend immédiatement la main, sans attendre la fin du délai ;
  • ligne 20 : ce message s'affiche donc avant celui de la ligne 16, alors qu'il est écrit après dans le code ;
  • ligne 23 : function heure(début?: moment.Moment): string — le paramètre début est optionnel (?), typé moment.Moment ; si fourni, la fonction calcule et ajoute une durée écoulée.
npx tsx async/async-01.ts

Résultat de l'exécution :

1
2
3
[début du script], heure=07:41:36:456
[fin du code principal du script], heure=07:41:36:457, durée= 0 seconde(s) et 2 millisecondes
[fin de l'action asynchrone setTimeout], heure=07:41:37:458, durée= 1 seconde(s) et 3 millisecondes

On observe que l'action asynchrone setTimeout se termine environ 1 seconde après la fin du code principal — c'est bien le délai programmé. Le script global, lui, n'est terminé qu'une fois cette tâche asynchrone achevée.

Remarque : setTimeout va nous servir à simuler des tâches asynchrones tout au long de ce chapitre. Une vraie tâche asynchrone (requête réseau, lecture de fichier...) se comporte de la même façon : elle rend la main immédiatement, puis signale son résultat plus tard — généralement via deux événements possibles, l'un de succès, l'autre d'échec.

11.2. script [async-02]

Ce script montre comment une fonction asynchrone peut communiquer son résultat en émettant des événements, à l'aide de la classe native EventEmitter (module node:events) :

'use strict';

// les fonctions asynchrones peuvent rendre un résultat en émettant un événement
// le code principal peut récupérer ces résultats en s'abonnant aux événements émis

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// un émetteur d'événements
const eventEmitter = new EventEmitter();

// setTimeout arme un timer de 1000 ms (2ième paramètre) et retourne immédiatement le n° de ce timer
// lorsque le timer a épuisé les 1000 ms il émet un événement qui est mis en file d'attente du runtime
// lorsque l'événement est traité par le runtime, la fonction (1er paramètre) est exécutée
setTimeout(function () {
  // ce code sera exécuté lorsque le timer aura atteint la valeur 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // on émet un événement pour dire qu'un résultat est disponible
  eventEmitter.emit("timer1Success", { success: 4 });
  // on émet un autre événement pour dire qu'un autre résultat est disponible
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)

// on s'abonne à l'évt [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});

// on s'abonne à l'évt [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// s'affichera avant les msg des evts émis par la fonction associée à [timer1]
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire d'affichage heure et durée
function heure(début?: moment.Moment): string {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // faut-il calculer une durée ?
  if (début) {
    const durée = now.valueOf() - début.valueOf();
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage heure + durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}
  • ligne 9 : import EventEmitter from 'events' — importe la classe qui permet d'émettre et d'écouter des événements ;
  • ligne 15 : const eventEmitter = new EventEmitter() — un émetteur d'événements est instancié avec new ;
  • lignes 24 et 26 : la fonction encapsulée dans setTimeout émet deux événements, timer1Success et timer1Failure, chacun avec sa propre donnée associée (ici, à titre de démonstration seulement — normalement une même tâche n'émettrait que l'un ou l'autre) ;
  • lignes 29-37 : le code principal s'abonne à ces deux événements avec eventEmitter.on(nomÉvénement, callback) — le callback ne s'exécutera qu'au moment où l'événement sera réellement émis, pas au moment de l'abonnement ;
  • ligne 40 : le code principal se termine ici, mais le script global ne se termine que lorsque la tâche asynchrone (et les callbacks abonnés) ont fini de s'exécuter.
npx tsx async/async-02.ts

Résultat de l'exécution :

1
2
3
4
5
[début du script], heure=07:41:38:163
[fin du code principal du script], heure=07:41:38:170, durée= 0 seconde(s) et 7 millisecondes
[setTimeout, fin du timer d'1 s], heure=07:41:39:173, durée= 1 seconde(s) et 10 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=07:41:39:173, durée= 1 seconde(s) et 10 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=07:41:39:173, durée= 1 seconde(s) et 10 millisecondes, via l'événement [timer1Failure]

11.3. script [async-03]

La boucle d'événements de node.js n'exécute qu'un seul événement à la fois : le suivant n'est traité qu'une fois le précédent terminé. Un code synchrone qui monopolise longtemps le processeur retarde donc le traitement de tous les événements en attente — y compris ceux déjà émis. Ce script (identique à async-02, avec une boucle intensive ajoutée) le démontre :

'use strict';

// les fonctions asynchrones peuvent rendre un résultat en émettant un événement
// le code principal peut récupérer ces résultats en s'abonnant aux événements émis

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// un émetteur d'événements
const eventEmitter = new EventEmitter();

// setTimeout arme un timer de 1000 ms (2ième paramètre) et retourne immédiatement le n° de ce timer
// lorsque le timer a épuisé les 1000 ms il émet un événement qui est mis en file d'attente du runtime
// lorsque l'événement est traité par le runtime, la fonction (1er paramètre) est exécutée
setTimeout(function () {
  // ce code sera exécuté lorsque le timer aura atteint la valeur 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // on émet un événement pour dire qu'un résultat est disponible
  eventEmitter.emit("timer1Success", { success: 4 });
  // on émet un autre événement pour dire qu'un autre résultat est disponible
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)

// on s'abonne à l'évt [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});

// on s'abonne à l'évt [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// un code synchrone un peu intensif qui a empêcher le code principal de s'achever avant la fin de [timer1]
for (let i = 0; i < 1000000; i++) {
  for (let j = 0; j < 10000; j++) {
    i + i ^ 2 + i ^ 3;
  }
}

// s'affichera avant les msg des evts émis par la fonction associée à [timer1]
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire d'affichage heure et durée
function heure(début?: moment.Moment): string {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // faut-il calculer une durée ?
  if (début) {
    const durée = now.valueOf() - début.valueOf();
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage heure + durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}
  • lignes 40-44 : une double boucle purement synchrone, sans aucun rapport avec la tâche asynchrone, mais qui occupe le processeur pendant plusieurs secondes ;
  • le timer d'1 seconde a fini son délai bien avant la fin de cette boucle — mais l'événement qu'il a émis doit attendre que le code synchrone rende la main à la boucle d'événements pour être enfin traité.
npx tsx async/async-03.ts

Résultat de l'exécution :

1
2
3
4
5
[début du script], heure=07:41:39:891
[fin du code principal du script], heure=07:41:45:623, durée= 5 seconde(s) et 732 millisecondes
[setTimeout, fin du timer d'1 s], heure=07:41:45:625, durée= 5 seconde(s) et 734 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=07:41:45:625, durée= 5 seconde(s) et 734 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=07:41:45:625, durée= 5 seconde(s) et 734 millisecondes, via l'événement [timer1Failure]

Le code principal a mis environ 5,7 secondes à s'exécuter (durée variable selon la machine), alors que le timer avait fini son délai d'une seconde depuis longtemps : l'événement associé a dû patienter que la boucle synchrone se termine. On retiendra qu'un code synchrone long doit, autant que possible, être décomposé en tâches asynchrones plus courtes.

11.4. Le mécanisme Promise

Le type [Promise] est une classe qui évite de gérer explicitement des événements : c'est fait implicitement, mais comprendre les événements sous-jacents aide à mieux appréhender son fonctionnement. Son constructeur admet en paramètre une fonction asynchrone à qui il transmet deux fonctions, traditionnellement nommées [resolve] et [reject] :

const promise = new Promise(function (resolve, reject) {
  // une tâche asynchrone est lancée
  // ...
  // si réussite : appeler resolve(result) où [result] est le résultat de la tâche
  // si échec : appeler reject(error) où [error] encapsule l'erreur rencontrée
});

Un objet Promise peut être dans quatre états :

  • [pending] : la tâche asynchrone n'est pas encore terminée ;
  • [fulfilled] : elle s'est terminée avec succès ;
  • [rejected] : elle s'est terminée sur un échec ;
  • [settled] : elle est terminée (succès ou échec, peu importe).

On s'abonne aux résultats d'une Promise avec la syntaxe promise.then(f1).catch(f2).finally(f3) : f1 s'exécute en cas de succès (elle reçoit le résultat transmis par resolve), f2 en cas d'échec (elle reçoit l'erreur transmise par reject), f3 dans tous les cas, sans paramètre.

11.5. script [async-04]

Ce script crée deux Promise indépendantes, chacune encapsulant un setTimeout, et s'abonne à leurs résultats :

'use strict';

// il est possible d'obtenir les résultats (success, failure) d'une fonction asynchrone
// sans utiliser explicitement des événements grâce à la classe [Promise]
// cette classe utilise implicitement des événements mais ceux-ci ne se voient pas dans le code

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure(débutScript));

// définition d'une tâche asynchrone à l'aide d'une promesse [Promise]
// la tâche asynchrone est le paramètre du constructeur [Promise]
const débutPromise1 = moment(Date.now());
const promise1 = new Promise<string>(function (resolve) {
  // log
  console.log("[début fonction asynchrone de promise1],", heure(débutPromise1));
  // code asynchrone
  setTimeout(function () {
    // ce code est exécuté après un délai d'1 seconde (2ième paramètre)
    console.log("[fin fonction asynchrone de promise1],", heure(débutPromise1));
    // la tâche asynchrone rend un résultat avec la fonction [resolve]
    // la promesse est alors réussie
    resolve('[réussite]');
  }, 1000)
});

// on peut connaître le résultat de la promesse [promise1]
// lorsque celle-ci a été résolue (resolve) ou rejetée (reject)
// l'instruction qui suit est un abonnement à l'évt [resolved] via la méthode [then]
// et à l'évt [rejected] via la méthode [catch]
// la méthode [finally] est exécutée que ce soit après un then ou un catch
promise1.then(result => {
  // cas de réussite de la promesse  [evt resolved]
  console.log(sprintf("[promise1.then], %s, result=%s", heure(débutPromise1), result));
}).catch(result => {
  // cas d'erreur  [evt rejected]
  console.log(sprintf("[promise1.catch], %s, result=%s", heure(débutPromise1), result));
}).finally(() => {
  // exécuté dans tous les cas
  console.log("[promise1.finally]", heure(débutPromise1));
});

// définition d'une tâche asynchrone à l'aide d'une promesse [Promise]
const débutPromise2 = moment(Date.now());
const promise2 = new Promise<string>(function (resolve, reject) {
  // log
  console.log("[début fonction asynchrone de promise2],", heure(débutPromise2));
  // tâche asynchrone
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise2],", heure(débutPromise2));
    // la tâche asynchrone rend un résultat avec la fonction [reject]
    // la promesse est alors ratée
    reject('[échec]');
  }, 2000)
});

// on peut connaître le résultat de la promesse [promise2]
// lorsque celle-ci a été résolue (resolve) ou rejetée (reject)
promise2.then(result => {
  // cas de réussite de la promesse [evt resolved]
  console.log(sprintf("[promise2.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // cas d'erreur [evt rejected]
  console.log(sprintf("[promise2.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // exécuté dans tous les cas
  console.log(sprintf("[promise2.finally], %s", heure(débutPromise2)));
});

// s'affichera avant les msg des fonctions asynchrones et ceux des évts associés
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire
function heure(début?: moment.Moment): string {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now.valueOf() - début.valueOf();
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}
  • ligne 18 : new Promise<string>(function (resolve) {...}) — TypeScript permet de préciser, via le paramètre générique <string>, le type de la valeur que resolve transmettra ; sans lui, ce type serait déduit unknown ;
  • promise1 se résout avec succès après 1 seconde (resolve('[réussite]')) ; promise2 échoue après 2 secondes (reject('[échec]')) ;
  • le code principal (ligne 75) se termine bien avant que les deux promesses aient un résultat — le script global, lui, continue jusqu'à ce qu'elles soient toutes deux réglées (*settled*).
npx tsx async/async-04.ts

Résultat de l'exécution :

[début du script], heure=07:42:29:437, durée= 0 seconde(s) et 0 millisecondes
[début fonction asynchrone de promise1], heure=07:42:29:443, durée= 0 seconde(s) et 0 millisecondes
[début fonction asynchrone de promise2], heure=07:42:29:444, durée= 0 seconde(s) et 0 millisecondes
[fin du code principal du script], heure=07:42:29:444, durée= 0 seconde(s) et 7 millisecondes
[fin fonction asynchrone de promise1], heure=07:42:30:445, durée= 1 seconde(s) et 2 millisecondes
[promise1.then], heure=07:42:30:446, durée= 1 seconde(s) et 3 millisecondes, result=[réussite]
[promise1.finally] heure=07:42:30:447, durée= 1 seconde(s) et 4 millisecondes
[fin fonction asynchrone de promise2], heure=07:42:31:445, durée= 2 seconde(s) et 1 millisecondes
[promise2.catch], heure=07:42:31:446, durée= 2 seconde(s) et 2 millisecondes, result=[échec]
[promise2.finally], heure=07:42:31:446, durée= 2 seconde(s) et 2 millisecondes

11.6. script [async-05]

On définit le plus souvent une fonction asynchrone rendant une Promise, plutôt que de créer directement l'objet Promise — pratique quand elle a besoin de paramètres. Ce script définit deux fonctions asynchrones et attend qu'elles se terminent toutes les deux avec [Promise.all] :

'use strict';

// on peut définir des fonctions asynchrones qui rendent un type [Promise]
// elles peuvent être alors taguées avec le mot clé [async]
// là encore
// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure());

// forme du résultat rendu par async01 et async02
interface RésultatAsync {
  prop1: number[];
  prop2: string;
  prop3: number;
}

// une fonction asynchrone peut rendre une promesse [Promise]
// et avoir alors l'attribut [async]
async function async01(p1: number): Promise<RésultatAsync> {
  return new Promise<RésultatAsync>(resolve => {
    console.log("[début de la tâche asynchrone async01]");
    // la tâche asynchrone
    const débutAsync01 = moment(Date.now());
    setTimeout(function () {
      // ce code est exécuté après un délai d'1 seconde (2ième paramètre)
      console.log("[fin de la tâche asynchrone async01],", heure(débutAsync01));
      // la tâche asynchrone peut rendre un résultat complexe
      resolve({
        prop1: [10, 20, 30],
        prop2: "abcd",
        prop3: p1,
      });
    }, 1000)
  });
}

// une fonction peut rendre une promesse [Promise]
// et peut alors avoir l'attribut [async]
async function async02(p1: number, p2: number): Promise<RésultatAsync> {
  return new Promise<RésultatAsync>(resolve => {
    console.log("[début de la tâche asynchrone async02]");
    // tâche asynchrone
    const débutAsync02 = moment(Date.now());
    setTimeout(function () {
      // le code suivant est exécuté après un délai de 2 secondes (2ième paramètre)
      console.log("[fin de la tâche asynchrone async02],", heure(débutAsync02));
      // la tâche asynchrone peut rendre un résultat complexe
      resolve({
        prop1: [11, 21, 31],
        prop2: "xyzt",
        prop3: p1 + p2
      });
    }, 2000)
  })
}

// on lance les deux fonctions asynchrones en parallèle
// et on attend qu'elles aient terminé toutes les deux
// le then ne s'exécutera que si les deux fonctions ont émis l'évt [resolved]
// le catch s'exécutera dès que l'une des deux fonctions émet l'évt [rejected]
Promise.all([async01(10), async02(10, 20)])
  // le résultat est un tableau [result1, result2] où [result1] est le résultat émis par un [resolve] de [async01]
  // et [result2] le résultat émis par un [resolve] de [async02]
  .then(result => {
    console.log(sprintf("[promise-all success], %s, result=%j", heure(débutScript), result));
  })
  // error est le résultat émis par le premier [reject] de l'une des deux fonctions asynchrones
  .catch(error => {
    console.log(sprintf("[promise-all error], %s, erreur=%j", heure(débutScript), error));
  })
  // finally est exécuté après le then ou le catch
  .finally(() => {
    console.log(sprintf("[promise-all finally], %s", heure(débutScript)));
  });

// s'affichera avant les msgs des fonctions asynchrones et des évts associés
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire
function heure(début?: moment.Moment): string {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now.valueOf() - début.valueOf();
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}
  • lignes 20 et 36 : les fonctions asynchrones sont typées Promise<RésultatAsync> grâce à l'interface RésultatAsync définie plus haut ;
  • ligne 65 : [Promise.all([...])] attend que toutes les promesses du tableau soient résolues avant d'exécuter son then — dès que l'une échoue, son catch s'exécute immédiatement, sans attendre les autres ;
  • les deux tâches se déroulent en parallèle (leurs attentes respectives se chevauchent) : la durée totale (~2s) correspond à la plus longue des deux, pas à leur somme (~3s).
npx tsx async/async-05.ts

Résultat de l'exécution :

1
2
3
4
5
6
7
8
[début du script], heure=07:42:32:179
[début de la tâche asynchrone async01]
[début de la tâche asynchrone async02]
[fin du code principal du script], heure=07:42:32:180, durée= 0 seconde(s) et 6 millisecondes
[fin de la tâche asynchrone async01], heure=07:42:33:181, durée= 1 seconde(s) et 1 millisecondes
[fin de la tâche asynchrone async02], heure=07:42:34:182, durée= 2 seconde(s) et 2 millisecondes
[promise-all success], heure=07:42:34:183, durée= 2 seconde(s) et 9 millisecondes, result=[{"prop1":[10,20,30],"prop2":"abcd","prop3":10},{"prop1":[11,21,31],"prop2":"xyzt","prop3":30}]
[promise-all finally], heure=07:42:34:183, durée= 2 seconde(s) et 9 millisecondes

11.7. script [async-06] — async / await

Les mots clés [async]/[await] permettent d'écrire du code asynchrone qui ressemble à du code synchrone : la gestion des événements sous-jacents est complètement cachée, ce qui facilite grandement la lecture. Ce script définit trois fonctions asynchrones — la 3ᵉ échoue volontairement — et les exécute d'abord séquentiellement, puis en parallèle :

'use strict';

// exécution parallèle ou séquentielle de plusieurs tâches asynchrones
// avec les mots clés async / await

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// début
const débutScript = moment(Date.now());
console.log("[début du code principal du script],", heure());

// forme du résultat rendu par async01/async02/async03
interface RésultatAsync {
  prop1: number[];
  prop2: string;
}

// une fonction asynchrone rendant une [Promise]
async function async01(débutAsync01: moment.Moment): Promise<RésultatAsync> {
  return new Promise(function (resolve) {
    console.log("[début fonction asynchrone async01],", heure());
    // fonction asynchrone
    setTimeout(function () {
      console.log("[fin fonction asynchrone async01],", heure(débutAsync01));
      // l'action asynchrone peut rendre un résultat complexe
      // ici réussite
      resolve({
        prop1: [11, 21, 31],
        prop2: "abcd"
      });
    }, 1000)
  });
}

// une fonction asynchrone rendant une [Promise]
async function async02(débutAsync02: moment.Moment): Promise<RésultatAsync> {
  console.log("[début fonction asynchrone async02],", heure());
  return new Promise(function (resolve) {
    // fonction asynchrone
    setTimeout(function () {
      console.log("[fin fonction asynchrone async02],", heure(débutAsync02));
      // l'action asynchrone peut rendre un résultat complexe
      // ici réussite
      resolve({
        prop1: [12, 22, 32],
        prop2: "xyzt"
      });
    }, 2000)
  })
}

// une fonction asynchrone rendant une [Promise]
async function async03(débutAsync03: moment.Moment): Promise<RésultatAsync> {
  console.log("[début fonction asynchrone async03],", heure());
  return new Promise((resolve, reject) => {
    // fonction asynchrone
    setTimeout(function () {
      console.log("[fin fonction asynchrone async03],", heure(débutAsync03));
      // l'action asynchrone peut rendre un résultat complexe
      // ici échec
      reject({
        prop1: [13, 23, 33],
        prop2: "échec"
      });
    }, 3000)
  })
}

// fonction asynchrone - utilisation async / await
async function main(): Promise<void> {
  const débutSequential = moment(Date.now());
  // exécution séquentielle des tâches asynchrones
  console.log("------------ exécution séquentielle des tâches asynchrones lancée ------------------------")
  try {
    // exécution avec attente de [async01]
    const débutAsync01 = moment(Date.now());
    const result1 = await async01(débutAsync01);
    console.log("[async01 result]=", result1);
    // exécution avec attente de [async02]
    const débutAsync02 = moment(Date.now());
    console.log("début async02-------------", heure());
    const result2 = await async02(débutAsync02);
    console.log("[async02 result]=", result2);
    // exécution avec attente de [async03]
    const débutAsync03 = moment(Date.now());
    console.log("début async03-------------", heure());
    const result3 = await async03(débutAsync03);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // une des actions asynchrones a échoué
    console.log(sprintf("[sequential error]= %j, %s", error, heure(débutSequential)));
  } finally {
    // terminé
    console.log("[fin exécution séquentielle des tâches asynchrones],", heure(débutSequential));
  }

  const débutParallel = moment(Date.now());
  // exécution en parallèle des tâches asynchrones
  console.log("------------ exécution parallèle des tâches asynchrones lancée ------------------------");
  try {
    const result = await Promise.all([async01(débutParallel), async02(débutParallel), async03(débutParallel)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // une des actions asynchrones a échoué
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // terminé
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
  }

  // terminé
  console.log("[fin de la fonction main],", heure(débutSequential));
}
// exécution fonction asynchrone main
main();

// s'affichera avant les différents msgs des fonctions asynchrones et de leurs évts
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire
function heure(début?: moment.Moment): string {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now.valueOf() - début.valueOf();
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}
  • ligne 72 : async function main(): Promise<void> — une fonction taguée async rend toujours une Promise, même si son corps ne le montre pas explicitement ;
  • ligne 79 : await async01(débutAsync01) suspend l'exécution de main jusqu'à ce que la promesse soit résolue, sans bloquer le reste du programme (le await ne peut s'utiliser qu'à l'intérieur d'une fonction async, ou au premier niveau d'un module — voir chapitre « Les modules », script import-04) ;
  • lignes 76-90 : exécution séquentielle — chaque await attend la fin de la tâche précédente avant de lancer la suivante ; la durée totale est la somme des trois délais (~6s) ;
  • lignes 102-111 : exécution en parallèle, avec await Promise.all([...]) — la durée totale est celle de la tâche la plus longue (~3s) ;
  • un try/catch classique suffit à intercepter l'échec d'une promesse await-ée — plus besoin de .catch() chaîné comme aux scripts précédents.
npx tsx async/async-06.ts

Résultat de l'exécution :

[début du code principal du script], heure=07:42:34:907
------------ exécution séquentielle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=07:42:34:914
[fin du code principal du script], heure=07:42:34:915, durée= 0 seconde(s) et 8 millisecondes
[fin fonction asynchrone async01], heure=07:42:35:916, durée= 1 seconde(s) et 2 millisecondes
[async01 result]= { prop1: [ 11, 21, 31 ], prop2: 'abcd' }
début async02------------- heure=07:42:35:917
[début fonction asynchrone async02], heure=07:42:35:918
[fin fonction asynchrone async02], heure=07:42:37:920, durée= 2 seconde(s) et 3 millisecondes
[async02 result]= { prop1: [ 12, 22, 32 ], prop2: 'xyzt' }
début async03------------- heure=07:42:37:921
[début fonction asynchrone async03], heure=07:42:37:921
[fin fonction asynchrone async03], heure=07:42:40:925, durée= 3 seconde(s) et 4 millisecondes
[sequential error]= {"prop1":[13,23,33],"prop2":"échec"}, heure=07:42:40:925, durée= 6 seconde(s) et 15 millisecondes
[fin exécution séquentielle des tâches asynchrones], heure=07:42:40:925, durée= 6 seconde(s) et 15 millisecondes
------------ exécution parallèle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=07:42:40:926
[début fonction asynchrone async02], heure=07:42:40:926
[début fonction asynchrone async03], heure=07:42:40:926
[fin fonction asynchrone async01], heure=07:42:41:927, durée= 1 seconde(s) et 2 millisecondes
[fin fonction asynchrone async02], heure=07:42:42:925, durée= 2 seconde(s) et 0 millisecondes
[fin fonction asynchrone async03], heure=07:42:43:926, durée= 3 seconde(s) et 1 millisecondes
[parallel error], heure=07:42:43:927, durée= 3 seconde(s) et 2 millisecondes, erreur={"prop1":[13,23,33],"prop2":"échec"}
[fin exécution parallèle des tâches asynchrones],heure=07:42:43:927, durée= 3 seconde(s) et 2 millisecondes
[fin de la fonction main], heure=07:42:43:927, durée= 9 seconde(s) et 17 millisecondes

On retrouve nettement la différence de durée totale : ~6s en séquentiel contre ~3s en parallèle pour les mêmes trois tâches (1s + 2s + 3s).

11.8. script [async-07] — compléments à Promise

[NOUVEAU depuis 2019] Ce script présente quatre compléments apparus entre 2020 et 2024, aujourd'hui très utilisés en TypeScript moderne :

'use strict';
// ========================================================================
// [NOUVEAU depuis 2019] compléments à la classe Promise
// ========================================================================

// petite fonction utilitaire : renvoie une promesse qui réussit ou échoue après [délai] ms
function tâche(nom: string, délai: number, réussit: boolean): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    setTimeout(() => {
      if (réussit) {
        resolve(`résultat de ${nom}`);
      } else {
        reject(`échec de ${nom}`);
      }
    }, délai);
  });
}

// ------------------------------------------------------------------------
// 1) Promise.allSettled()  (ECMAScript 2020)
// ------------------------------------------------------------------------
// async-05.js montre Promise.all() : si UNE des promesses échoue, le catch est déclenché
// et on perd le résultat des autres promesses qui, elles, avaient réussi
// Promise.allSettled() attend TOUJOURS que toutes les promesses soient terminées
// (réussies ou en échec) et rend le détail de chacune, sans jamais rejeter
async function démoAllSettled(): Promise<void> {
  console.log("--- Promise.allSettled ---");
  const résultats = await Promise.allSettled([
    tâche("tâche1", 300, true),
    tâche("tâche2", 200, false),
    tâche("tâche3", 100, true)
  ]);
  // chaque élément a la forme { status: "fulfilled", value: ... } ou { status: "rejected", reason: ... }
  résultats.forEach((résultat, index) => {
    if (résultat.status === "fulfilled") {
      console.log(`tâche${index + 1} réussie :`, résultat.value);
    } else {
      console.log(`tâche${index + 1} échouée :`, résultat.reason);
    }
  });
}

// ------------------------------------------------------------------------
// 2) Promise.any()  (ECMAScript 2021)
// ------------------------------------------------------------------------
// rend le résultat de la PREMIÈRE promesse qui réussit (les autres sont ignorées)
// ne rejette que si TOUTES les promesses échouent (avec une AggregateError regroupant les erreurs)
async function démoAny(): Promise<void> {
  console.log("--- Promise.any ---");
  try {
    const résultat = await Promise.any([
      tâche("miroir-lent", 500, true),
      tâche("miroir-rapide", 100, true),
      tâche("miroir-en-panne", 50, false)
    ]);
    console.log("premier miroir disponible :", résultat);
  } catch (erreurGlobale: any) {
    // erreurGlobale.errors contient la liste de toutes les erreurs individuelles
    console.log("tous les miroirs ont échoué :", erreurGlobale.errors);
  }
}

// ------------------------------------------------------------------------
// 3) Promise.withResolvers()  (ECMAScript 2024)
// ------------------------------------------------------------------------
// avant, pour "sortir" les fonctions resolve/reject du constructeur Promise
// (par ex. pour les appeler bien plus tard, ailleurs dans le code), il fallait
// une variable intermédiaire un peu artificielle :
//   let resolveExterne;
//   const promesse = new Promise(resolve => { resolveExterne = resolve; });
// Promise.withResolvers() fait ça directement, en une seule expression
function démoWithResolvers(): Promise<string> {
  console.log("--- Promise.withResolvers ---");
  const { promise, resolve } = Promise.withResolvers<string>();

  // on peut résoudre la promesse depuis n'importe où, par exemple dans un autre callback
  setTimeout(() => {
    console.log("[callback externe] on déclenche la résolution de la promesse");
    resolve("valeur transmise depuis un callback externe");
  }, 200);

  return promise;
}

// ------------------------------------------------------------------------
// 4) annuler une opération asynchrone avec AbortController
// ------------------------------------------------------------------------
// AbortController permet d'annuler une opération asynchrone en cours (fetch, timers, ...)
// c'est le mécanisme standard utilisé par [fetch] pour les délais d'attente (cf http/fetch-01.js)
function tâcheAnnulable(délai: number, signal: AbortSignal): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    const timer = setTimeout(() => resolve("terminé normalement"), délai);
    // si le signal d'annulation est déclenché, on arrête tout et on rejette
    signal.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(new Error("opération annulée : " + signal.reason));
    });
  });
}

async function démoAbortController(): Promise<void> {
  console.log("--- AbortController ---");
  const contrôleur = new AbortController();
  // on programme l'annulation dans 100 ms, avant la fin normale de la tâche (500 ms)
  setTimeout(() => contrôleur.abort("délai dépassé côté utilisateur"), 100);
  try {
    const résultat = await tâcheAnnulable(500, contrôleur.signal);
    console.log("résultat =", résultat);
  } catch (erreur: any) {
    console.log("erreur :", erreur.message);
  }
}

// exécution successive des démonstrations
async function main(): Promise<void> {
  await démoAllSettled();
  await démoAny();
  console.log("--- Promise.withResolvers, résultat ---", await démoWithResolvers());
  await démoAbortController();
}

main();
  • [Promise.allSettled] (ES2020) : contrairement à Promise.all (script async-05), elle attend toujours que toutes les promesses soient terminées, réussies ou non, et rend le détail de chacune — utile quand on ne veut jamais perdre le résultat des tâches qui ont réussi, même si une autre a échoué ;
  • [Promise.any] (ES2021) : rend le résultat de la première promesse qui réussit, en ignorant les autres — ne rejette que si toutes échouent ;
  • [Promise.withResolvers] (ES2024) : extrait resolve et reject du constructeur Promise en une seule expression, utile pour résoudre une promesse plus tard, depuis un autre endroit du code ;
  • [AbortController] : permet d'annuler une opération asynchrone en cours — c'est le mécanisme standard utilisé par fetch pour les délais d'attente (voir chapitre « Les fonctions HTTP »).
npx tsx async/async-07.ts

Résultat de l'exécution :

--- Promise.allSettled ---
tâche1 réussie : résultat de tâche1
tâche2 échouée : échec de tâche2
tâche3 réussie : résultat de tâche3
--- Promise.any ---
premier miroir disponible : résultat de miroir-rapide
--- Promise.withResolvers ---
[callback externe] on déclenche la résolution de la promesse
--- Promise.withResolvers, résultat --- valeur transmise depuis un callback externe
--- AbortController ---
erreur : opération annulée : délai dépassé côté utilisateur