Retour

Async / await

6 nov. 2019
11mn

Après avoir vu en profondeur comment fonctionnent les promesses et comment les utiliser, nous allons nous pencher sur une fonctionnalité arrivée avec l'ES7 : async/await. Async await nous permet de résoudre un problème né de l'utilisation de code asynchrone en javascript : le callback hell, récurrent lorsque l'on cherche à faire de l'asynchrone de manière séquentielle. Async/await n'est globalement que du sucre syntaxique, qui nous permet de lire du code asynchrone comme s'il était synchrone.

⚠️ Il est important de connaître et de comprendre les promesses pour maîtriser async/await. Si ce n'est pas votre cas, je vous conseille de lire mon précédent article qui traite justement des promesses.

Définition

async

Mettre le mot async devant une fonction lui donne l'instruction de retourner une promesse. Si une erreur apparaît pendant l'exécution, la promesse est rejetée. Si la fonction retourne une valeur, la promesse est résolue avec cette valeur. Et si une promesse est retournée, elle reste inchangée.

async function asyncFunction() { // équivaut à : return Promise.resolve('résultat'); return 'résultat'; } asyncFunction().then(console.log) // "résultat"

les fonctions async rendent le code beaucoup plus concis que l'utilisation de promesses normales :

// Utilisation du then const makeRequest = () => getJSON() .then(data => { console.log(data) return "done" }) makeRequest() // Utilisation d'async const makeRequest = async () => { console.log(await getJSON()) return "done" } makeRequest()

await

Le mot-clé await ne peut être utilisé qu'à l'intérieur d'une fonction async. Il nous permet d'attendre que la promesse retourne son résultat (ou une erreur) pour continuer l'exécution du code.

async function getAsyncNumber1() // renvoie une promesse avec comme valeur un nombre async function getAsyncNumber2() // même chose que ligne du dessus const getAsyncAddition = async () => { const number1 = await getAsyncNumber1(); const number2 = await getAsyncNumber2(); return number1 + number2; }

Pour avoir le même résultat sans await, il faudrait

  • soit imbriquer un then() dans un autre :
const getAsyncAddition = () => getAsyncNumber1().then(number1 => getAsyncNumber2().then(number2 => number1 + number2));

Ce qui, vous en conviendrez, est difficile à lire.

  • soit déclarer une variable dans le scope de la fonction :
const getAsyncAddition = () => { let number; return getAsyncNumber1() .then(vnumber => { number1 = vnumber1; return getAsyncNumber2(); }) .then(number2 => number1 + number2); }

Voici une liste de règles importantes à retenir sur async await :

  • les fonctions async retournent une promesse.
  • les fonctions async utilisent une Promise implicite pour retourner un résultat. Même si une promesse n'est pas retournée explicitement, la fonction async fait en sorte que le code soit passé par une promesse.
  • await bloque l'exécution du code à l'intérieur d'une fonction async. Il permet de s'assurer que la prochaine ligne soit exécutée quand la promesse est résolue. Donc si du code asynchrone est déjà en train de s'exécuter, await n'aura pas d'effet sur lui.
  • il peut y avoir plusieurs await à l'intérieur d'une fonction async.
  • Il faut bien faire attention lors de l'utilisation d'await dans une boucle, car le code peut facilement s'exécuter de manière séquentielle au lieu d'être executé en parallèle.
  • await est toujours utilisé pour une seule promesse.

Pourquoi utiliser async await ?

On pourrait se demander pourquoi utiliser cette fonctionnalité, alors que les promesses existent déjà en JS.

  • Comme vous l'avez vu avec les exemples ci-dessus, async/await nous permet d'avoir un code beaucoup plus concis et nous évite le callback hell.
  • L'utilisation d'async/await permet d'améliorer la gestion d'erreur, car il est possible de gérer les erreurs synchrones et asynchrones en même temps (ce qui n'était pas le cas auparavant), notamment grâce à l'utilisation d'un try/catch.
const makeRequest = () => { try { getJSON() .then(result => { // this parse may fail const data = JSON.parse(result) console.log(data) }) // décommenter ce bloc pour gérer les erreurs asynchrones // .catch((err) => { // console.log(err) // }) } catch (err) { console.log(err) } }

Dans cet exemple, le try/catch va casser si le JSON.parse casse car il se passe à l'intérieur d'une promesse. Il est nécessaire d'utiliser un .catch() sur la promesse et donc dupliquer la gestion d'erreur. Voici le même code avec async/await :

const makeRequest = async () => { try { // this parse may fail const data = JSON.parse(await getJSON()) console.log(data) } catch (err) { console.log(err) } }

Mais il est aussi possible d'utiliser un catch à l'appel de la fonction avec async/await :

const doSomethingAsync = async () => { let result = await someAsyncCall(); return result; } doSomethingAsync(). .then(successHandler) .catch(errorHandler);
  • Il arrive assez souvent d'appeler une promesse (qu'on va appeler promesse1) et d'utiliser sa valeur de retour pour appeler une deuxième promesse (qui s'appelle sans surprise promesse2), pour ensuite utiliser le résultat de ces deux promesses pour appeler promesse3. Le code ressemble donc à ceci :
const makeRequest = () => { return promise1() .then(value1 => { // Du code return promise2(value1) .then(value2 => { // Du code return promise3(value1, value2) }) }) }

Il est possible ici d'englober les promesses 1 et 2 dans un Promise.all pour éviter d'avoir à imbriquer les promesses :

const makeRequest = () => { return promise1() .then(value1 => { // Du code return Promise.all([value1, promise2(value1)]) }) .then(([value1, value2]) => { // Du code return promise3(value1, value2) }) }

Le problème de cette approche est qu'elle sacrifie la sémantique au profit de la lisibilité. Il n'y a aucune raison de mettre les promesses 1 et 2 dans un tableau, excepté pour éviter l'imbriquation de promesses.

Ce qui est rendu beaucoup plus simple grâce à async/await :

const makeRequest = async () => { const value1 = await promise1() const value2 = await promise2(value1) return promise3(value1, value2) }
  • Le debugging est rendu beaucoup plus simple grâce à l'utilisation d'async/await. On peut mettre des points d'arrêts contrairement à l'utilisation de promesses.

  • Il est possible d'utiliser await sur des opérations synchrones et asynchrones. Par exemple, écrire await 5 revient à écrire Promise.resolve(5).

Top-level await

Avant de passer à la conclusion, j'aimerais aborder un sujet qui est encore en draft (au stage 3, donc probablement bientôt disponible !) au moment où j'écris ces lignes : le top-level await.

Le top-level await permet aux modules d'agir comme des fonctions async. Il était possible auparavant d'importer un module dans une fonction async, mais un problème se posait : l'export de ce module pouvait être accessible avant que la fonction async soit résolue. Cela pouvait poser problème car si un autre module importe le module présent dans la fonction async, le résultat était potentiellement undefined.

Un article écrit par Rich Harris a cependant mis en avant certaines craintes à propos de cette nouvelle proposition, notamment:

  • Top-level await pourrait bloquer l'exécution du code.
  • Top-level await pourrait bloquer le fetch des ressources.
  • Top-level await pourrait créer des problèmes d'intéropérabilité avec les modules CommonJS.

Pour résoudre ces problématiques, la version en stage 3 propose ces solutions :

  • Top-level await ne bloque pas les exécutions en parralèle.
  • Top-level await se produit durant la phase d'execution du module (qui correspond à la dernière phase, après l'initialization et la configuration), ce qui signifie qu'il se produit après que les ressources soient fetch.
  • Top-level await se limite aux modules, il ne supportera pas les scripts ou les modules CommonJS.

Une solution qui permettrait donc d'utiliser dynamiquement les modules en JavaScript mais qui soulève malgré tout plusieurs questions, car les imports impératifs sont plus lents et mauvais pour les performances d'une application que les imports déclaratifs.

Pour ceux qui aimeraient avoir plus d'informations sur ces deux sujets, voici quelques ressources :

Ces 3 ressources sont par contre en anglais, mais pour les anglophobes, il n'y a pas à s'inquièter car il est très probable que l'on entendra parler à nouveau du top-level await.

Conclusion

async/await est une fonctionnalité incroyable qui permet d'écrire de l'asynchrone facilement. Il est cependant important de comprendre que pour le langage, async/await fonctionne exactement comme une promesse et qu'il ne résoud pas tout les problèmes, comme la gestion de plusieurs appels asynchrones qui sont indépendants. Les fonctions async fonctionnent exactement comme les promesses, mais sont utiles pour gérer les erreurs, éviter d'imbriquer ses promesses, et lire du code asynchrone comme du code synchrone. J'espère que cet article vous aura aidé à y voir plus clair !


Articles sur le même thème

Comment créer de la dette technique dès le début d’un nouveau projet ?

Quand on arrive sur un projet existant, on doit souvent subir une dette technique qui nous fait perdre du temps et qui nous rend fou au point de vérifier qui a fait le code. Vous aussi vous voulez entrer dans la postérité lors d’un git blame et mal concevoir votre produit ?

9 août 202310mnMarianne Joseph-Géhannin

Maîtriser le temps lors du débogage grâce aux points d’arrêt

On ne va pas se mentir, la journée type d’un développeur est rythmée par des cafés et des “ça marche pô”. Et si le dev en question a choisi JS comme langage de programmation, ou de scripting si vous préférez, et bien, ça n'arrange pas les choses. Mais alors que faire quand “ça marche pô” ?

10 mai 202310mnGiuseppe Cunsolo

Découvrez Eleven Labs

Notre site pour mieux nous connaître

J'y vais

Contact

Eleven Labs - Paris

102, rue du Faubourg Saint Honoré

75008 Paris

Eleven Labs - Nantes

42, rue la Tour d'Auvergne

44200 Nantes

Eleven Labs - Montréal

1155, Metcalfe St Suite 1500

Montréal, QC H3B 2V6, Canada

business@eleven-labs.com

01.82.83.11.75