Qu'est-ce que c'est et comment l'utiliser
Lors du développement d'applications en JavaScript, il est fort probable que vous ayez croisé des fonctions asynchrones, telles que la fonction `fetch` dans le navigateur ou `readFile` dans Node.js.
Si vous avez utilisé ces fonctions comme vous le feriez avec des fonctions classiques, vous avez peut-être observé des comportements inattendus. Cela est dû à leur nature asynchrone. Cet article a pour but de clarifier ce concept et de vous montrer comment manier les fonctions asynchrones avec expertise.
Introduction aux fonctions synchrones
JavaScript, étant un langage mono-thread, ne peut exécuter qu'une seule opération à la fois. Cela signifie que lorsqu'il rencontre une fonction nécessitant un temps de traitement conséquent, il attend que son exécution soit entièrement achevée avant de poursuivre avec le reste du code.
La majorité des fonctions sont traitées de bout en bout par le processeur. Pendant cette période, et quelle que soit sa durée, le processeur est entièrement monopolisé. Ces fonctions sont qualifiées de synchrones. Voici un exemple illustrant ce concept :
function additionner(a, b) {
for (let i = 0; i < 1000000; i ++) {
// Ne rien faire
}
return a + b;
}
// L'appel de la fonction prendra du temps
somme = additionner(10, 5);
// Cependant, le processeur ne peut pas avancer tant que l'addition n'est pas terminée
console.log(somme);
Cette fonction effectue une boucle intensive qui prend un temps considérable avant de renvoyer la somme de ses deux arguments.
Après avoir défini la fonction, nous l'avons appelée et stocké le résultat dans la variable `somme`. Puis, nous avons affiché la valeur de cette variable. Bien que l'exécution de la fonction `additionner` prenne un certain temps, le processeur est incapable d'afficher la somme avant que l'exécution ne soit complètement terminée.
La plupart des fonctions que vous rencontrerez auront un comportement prévisible similaire à celui-ci. Néanmoins, certaines fonctions sont asynchrones et fonctionnent de manière distincte.
Introduction aux fonctions asynchrones
Les fonctions asynchrones délèguent la majeure partie de leur travail en dehors du processeur. Cela implique que même si l'exécution de la fonction peut prendre du temps, le processeur reste disponible et peut effectuer d'autres tâches.
Voici un exemple d'une telle fonction :
fetch('https://jsonplaceholder.typicode.com/users/1');
Afin d'améliorer l'efficacité, JavaScript permet au processeur de passer à d'autres tâches en attente d'utilisation du CPU, avant même que l'exécution de la fonction asynchrone ne soit achevée.
Comme le processeur progresse avant la fin de l'exécution de la fonction asynchrone, son résultat n'est pas immédiatement accessible. Il est en attente. Si le processeur tentait d'exécuter d'autres parties du code dépendantes de ce résultat, des erreurs se produiraient.
Par conséquent, le processeur doit exécuter uniquement les parties du programme qui ne dépendent pas de ce résultat en attente. Pour cela, JavaScript utilise des promesses.
Qu'est-ce qu'une promesse en JavaScript ?
En JavaScript, une promesse est une valeur temporaire retournée par une fonction asynchrone. Les promesses sont le fondement de la programmation asynchrone moderne en JavaScript.
Lorsqu'une promesse est créée, deux scénarios peuvent se produire : soit elle est résolue lorsque la valeur de retour de la fonction asynchrone est obtenue avec succès, soit elle est rejetée en cas d'erreur. Ces événements font partie du cycle de vie d'une promesse. Il est donc possible d'attacher des gestionnaires d'événements à la promesse pour qu'ils soient invoqués lorsqu'elle est résolue ou rejetée.
Tout code qui requiert la valeur finale d'une fonction asynchrone peut être associé au gestionnaire d'événements de la promesse pour sa résolution. Similairement, le code qui traite les erreurs d'une promesse rejetée sera associé à son gestionnaire d'événements correspondant.
Voici un exemple où nous lisons les données d'un fichier dans Node.js :
const fs = require('fs/promises');
fileReadPromise = fs.readFile('./hello.txt', 'utf-8');
fileReadPromise.then((data) => console.log(data));
fileReadPromise.catch((error) => console.log(error));
Dans la première ligne, nous importons le module `fs/promises`.
Dans la deuxième ligne, nous appelons la fonction `readFile`, en spécifiant le nom et l'encodage du fichier dont nous voulons lire le contenu. Cette fonction est asynchrone et renvoie donc une promesse que nous stockons dans la variable `fileReadPromise`.
Dans la troisième ligne, nous attachons un écouteur d'événement qui se déclenche lorsque la promesse est résolue. Pour ce faire, nous invoquons la méthode `then` sur l'objet promesse. L'argument passé à `then` est la fonction à exécuter lorsque la promesse est résolue.
Dans la quatrième ligne, nous attachons un écouteur pour le cas où la promesse est rejetée, en utilisant la méthode `catch` et en lui passant le gestionnaire d'erreurs comme argument.
Une autre méthode consiste à utiliser les mots-clés `async` et `await`, que nous allons aborder maintenant.
Explication de `async` et `await`
Les mots-clés `async` et `await` peuvent être utilisés pour écrire du JavaScript asynchrone avec une syntaxe plus agréable. Cette section explique comment utiliser ces mots-clés et leur impact sur le code.
Le mot-clé `await` sert à interrompre l'exécution d'une fonction en attendant qu'une fonction asynchrone soit terminée. Voici un exemple :
const fs = require('fs/promises');
function lireDonnees() {
const data = await fs.readFile('./hello.txt', 'utf-8');
// Cette ligne ne sera pas exécutée tant que les données ne seront pas disponibles
console.log(data);
}
lireDonnees()
Nous avons utilisé le mot-clé `await` lors de l'appel de `readFile`. Cela a ordonné au processeur d'attendre la lecture du fichier avant d'exécuter la ligne suivante (le `console.log`). Cela garantit que le code dépendant du résultat d'une fonction asynchrone ne s'exécute pas tant que le résultat n'est pas disponible.
Si vous essayez d'exécuter le code ci-dessus, vous rencontrerez une erreur. En effet, `await` ne peut être utilisé qu'à l'intérieur d'une fonction asynchrone. Pour déclarer une fonction comme asynchrone, vous utilisez le mot-clé `async` avant la déclaration de la fonction, comme ceci :
const fs = require('fs/promises');
async function lireDonnees() {
const data = await fs.readFile('./hello.txt', 'utf-8');
// Cette ligne ne sera pas exécutée tant que les données ne seront pas disponibles
console.log(data);
}
// Appel de la fonction pour l'exécuter
lireDonnees()
// Le code ici sera exécuté en attendant que la fonction lireDonnees se termine
console.log('En attente des données')
En exécutant cet extrait de code, vous observerez que JavaScript exécute le `console.log` externe pendant qu'il attend que les données du fichier texte soient disponibles. Une fois ces données disponibles, le `console.log` à l'intérieur de `lireDonnees` est exécuté.

La gestion des erreurs lors de l'utilisation de `async` et `await` se fait généralement à l'aide de blocs try/catch. Il est également crucial de savoir comment boucler avec du code asynchrone.
`Async` et `await` sont des fonctionnalités du JavaScript moderne. Historiquement, le code asynchrone était écrit à l'aide de callbacks.
Introduction aux callbacks
Un callback est une fonction qui est appelée une fois que le résultat est disponible. Tout code qui nécessite la valeur de retour est placé à l'intérieur du callback. Le reste du code, en dehors du callback, ne dépend pas du résultat et peut être exécuté sans attendre.
Voici un exemple qui lit un fichier dans Node.js :
const fs = require("fs");
fs.readFile("./hello.txt", "utf-8", (err, data) => {
// Dans ce callback, nous mettons tout le code qui requiert le résultat
if (err) console.log(err);
else console.log(data);
});
// Ici, nous pouvons effectuer les tâches qui ne requièrent pas le résultat
console.log("Bonjour depuis le programme")
Dans la première ligne, nous avons importé le module `fs`. Ensuite, nous avons appelé la fonction `readFile` de ce module. Cette fonction lit le contenu d'un fichier que nous spécifions. Le premier argument est le chemin du fichier, et le second spécifie son encodage.
La fonction `readFile` lit le contenu d'un fichier de manière asynchrone. Pour cela, elle prend une fonction en argument : un callback, qui sera appelé une fois que les données auront été lues.
Le premier argument passé au callback est une erreur, qui aura une valeur si une erreur se produit pendant l'exécution de la fonction. Sinon, elle sera indéfinie.
Le deuxième argument passé au callback est les données lues depuis le fichier. Le code à l'intérieur de ce callback accède donc aux données du fichier. Le code en dehors de ce callback n'a pas besoin des données du fichier et peut donc être exécuté pendant l'attente de ces données.
L'exécution du code ci-dessus produirait le résultat suivant :

Fonctionnalités clés de JavaScript
Certaines caractéristiques et fonctionnalités clés influencent le comportement du JavaScript asynchrone. Elles sont bien expliquées dans la vidéo suivante :

J'ai brièvement décrit les deux caractéristiques importantes ci-dessous.
#1. Mono-thread
Contrairement à d'autres langages qui permettent au programmeur d'utiliser plusieurs threads, JavaScript n'autorise qu'un seul thread. Un thread est une séquence d'instructions dépendantes les unes des autres. Plusieurs threads permettent au programme d'exécuter un thread différent lors de l'exécution d'opérations bloquantes.
Néanmoins, de multiples threads augmentent la complexité et rendent la compréhension des programmes plus difficile. Cela accroît le risque d'introduire des bugs et rend le débogage plus compliqué. JavaScript a été conçu mono-thread pour des raisons de simplicité. En tant que langage mono-thread, il s'appuie sur la gestion d'événements pour traiter efficacement les opérations bloquantes.
#2. Piloté par les événements
JavaScript est également piloté par les événements, ce qui signifie que divers événements se produisent lors du cycle de vie d'un programme JavaScript. En tant que programmeur, vous pouvez associer des fonctions à ces événements. Chaque fois que l'événement survient, la fonction associée est appelée et exécutée.
Certains événements peuvent découler de la disponibilité du résultat d'une opération bloquante. Dans ce cas, la fonction associée est appelée avec le résultat.
Points à considérer lors de l'écriture de JavaScript asynchrone
Dans cette dernière section, je vais aborder certains éléments à prendre en compte lors de l'écriture de code asynchrone en JavaScript. Cela inclut la prise en charge des navigateurs, les bonnes pratiques et l'importance du concept.
Prise en charge des navigateurs
Voici un tableau montrant la prise en charge des promesses dans différents navigateurs.
Source: caniuse.com
Voici un tableau indiquant la prise en charge des mots-clés `async` dans différents navigateurs.
Source: caniuse.com
Bonnes pratiques
- Privilégiez toujours `async/await`, car cela vous aide à écrire du code plus propre et plus facile à comprendre.
- Gérez les erreurs dans des blocs `try/catch`.
- N'utilisez le mot-clé `async` que lorsqu'il est nécessaire d'attendre le résultat d'une fonction.
Importance du code asynchrone
Le code asynchrone vous permet d'écrire des programmes plus efficaces qui utilisent un seul thread. Ceci est crucial car JavaScript est utilisé pour créer des sites web qui exécutent de nombreuses opérations asynchrones, telles que les requêtes réseau et la lecture/écriture de fichiers sur le disque. Cette efficacité a permis à des environnements d'exécution tels que Node.js de devenir populaires en tant que runtime privilégié pour les serveurs d'applications.
Conclusion
Cet article a été long, mais il nous a permis de comprendre en quoi les fonctions asynchrones diffèrent des fonctions synchrones classiques. Nous avons également vu comment utiliser le code asynchrone avec les promesses, les mots-clés `async/await` et les callbacks.
De plus, nous avons abordé les caractéristiques fondamentales de JavaScript. Dans la dernière partie, nous avons évoqué la prise en charge des navigateurs et les bonnes pratiques.
Pour aller plus loin, vous pouvez consulter les questions fréquentes d'entretien sur Node.js.