iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
JavaScript · Débutant

JavaScript

Apprendre JavaScript de zéro jusqu'au niveau expert : syntaxe moderne, DOM, asynchrone et event loop, modules, prototypes, métaprogrammation, performance et mémoire.

JavaScript ES2025Node.jsDOMVitestVite
01Introduction et environnement02Variables, types et coercition03Conditions et boucles04Fonctions, portée et closures05Objets, tableaux, Map et Set06Le DOM et les événements07Asynchrone : callbacks, promesses, async/await et event loop08Modules, npm, outillage et tests09Prototypes, classes et this10Itérateurs, générateurs, Symbol, Proxy et métaprogrammation11Niveau expert : moteur, performance, mémoire et architecture
Chapitre 7·55 min

Asynchrone : callbacks, promesses, async/await et event loop

JavaScript est mono-thread : une seule ligne s'exécute à la fois. Pourtant, une page charge des données, réagit aux clics et anime des éléments en même temps. Le secret s'appelle l'event loop. C'est le chapitre le plus important du cours : sans lui, l'ordre d'exécution du code JavaScript reste un mystère.

Le problème

console.log('A')
setTimeout(() => console.log('B'), 0)
console.log('C')
// A, C, B

Même avec un délai de 0, B s'affiche en dernier. setTimeout ne met pas le code en pause : il confie le callback à l'environnement (navigateur ou Node) et continue.

L'event loop

┌───────────────────────────┐
│        Call stack         │  ← code synchrone en cours
└─────────────┬─────────────┘
              │ vide ?
              ▼
┌───────────────────────────┐
│  File des microtâches     │  ← promesses (.then), queueMicrotask
│  (vidée ENTIÈREMENT)      │
└─────────────┬─────────────┘
              ▼
┌───────────────────────────┐
│  Rendu (navigateur)       │  ← requestAnimationFrame, style, paint
└─────────────┬─────────────┘
              ▼
┌───────────────────────────┐
│  File des tâches          │  ← setTimeout, événements, I/O
│  (UNE tâche par tour)     │
└───────────────────────────┘

À chaque tour : la pile d'appels se vide, toutes les microtâches s'exécutent, le navigateur peut dessiner, puis une tâche est prise dans la file.

console.log('1 sync')
setTimeout(() => console.log('5 tâche'), 0)
Promise.resolve().then(() => console.log('3 microtâche'))
queueMicrotask(() => console.log('4 microtâche'))
console.log('2 sync')
// 1 sync, 2 sync, 3 microtâche, 4 microtâche, 5 tâche

Conséquence pratique : une boucle de calcul de 3 secondes bloque tout, clics et animations compris. Le chapitre 11 montre comment découper ou déporter ce travail.

Callbacks

Le modèle historique : on passe une fonction appelée à la fin de l'opération.

import fs from 'node:fs'
 
fs.readFile('config.json', 'utf8', (err, contenu) => {
  if (err) return console.error(err)
  const config = JSON.parse(contenu)
  fs.readFile(config.cheminDonnees, 'utf8', (err, donnees) => {
    if (err) return console.error(err)
    // et ainsi de suite…
  })
})

Imbriquer trois ou quatre opérations produit le fameux « callback hell », et la gestion d'erreur doit être répétée à chaque niveau.

Promesses

Une promesse représente une valeur future. Elle est dans l'un de trois états : pending, fulfilled ou rejected, et ne change d'état qu'une fois.

const attendre = ms => new Promise(resolve => setTimeout(resolve, ms))
 
function chargerUser(id) {
  return new Promise((resolve, reject) => {
    if (id <= 0) reject(new Error('id invalide'))
    else setTimeout(() => resolve({ id, nom: 'Ada' }), 200)
  })
}
 
chargerUser(1)
  .then(user => chargerCommandes(user.id))
  .then(commandes => console.log(commandes))
  .catch(err => console.error('Échec :', err.message))
  .finally(() => masquerLoader())

Chaque .then renvoie une nouvelle promesse : on enchaîne à plat, et un seul .catch attrape une erreur survenue n'importe où dans la chaîne.

async / await

Du sucre syntaxique sur les promesses, qui permet d'écrire du code asynchrone comme du code synchrone.

async function afficherCommandes(id) {
  try {
    const user = await chargerUser(id)
    const commandes = await chargerCommandes(user.id)
    console.log(commandes)
  } catch (err) {
    console.error('Échec :', err.message)
  } finally {
    masquerLoader()
  }
}

Une fonction async renvoie toujours une promesse. await suspend la fonction (pas le programme) jusqu'à la résolution.

fetch

async function getJSON(url, { timeout = 8000, ...options } = {}) {
  const res = await fetch(url, { ...options, signal: AbortSignal.timeout(timeout) })
  if (!res.ok) throw new Error(`HTTP ${res.status} sur ${url}`)
  return res.json()
}
 
const articles = await getJSON('https://api.exemple.fr/articles')

fetch ne rejette pas sur une réponse 404 ou 500 : seulement sur une erreur réseau. Vérifier res.ok est obligatoire.

Parallèle contre séquentiel

// Séquentiel : ~600 ms
const a = await charger('a')   // 200 ms
const b = await charger('b')   // 200 ms
const c = await charger('c')   // 200 ms
 
// Parallèle : ~200 ms
const [a2, b2, c2] = await Promise.all([charger('a'), charger('b'), charger('c')])

Mettre await dans une boucle sur des opérations indépendantes est l'erreur de performance asynchrone la plus courante.

CombinateurRésout quandRejette quand
Promise.alltoutes réussissentla première échoue
Promise.allSettledtoutes terminéesjamais
Promise.racela première terminela première termine en échec
Promise.anyla première réussittoutes échouent
const resultats = await Promise.allSettled(urls.map(u => getJSON(u)))
const ok = resultats.filter(r => r.status === 'fulfilled').map(r => r.value)
const ko = resultats.filter(r => r.status === 'rejected').map(r => r.reason.message)

Limiter la concurrence

Promise.all sur 5 000 URLs lance 5 000 requêtes d'un coup. Pour en limiter le nombre :

async function mapLimite(items, limite, fn) {
  const resultats = new Array(items.length)
  let index = 0
  async function worker() {
    while (index < items.length) {
      const i = index++
      resultats[i] = await fn(items[i], i)
    }
  }
  await Promise.all(Array.from({ length: limite }, worker))
  return resultats
}
 
const pages = await mapLimite(urls, 10, url => getJSON(url))

Comme JavaScript est mono-thread, index++ n'a aucun risque de concurrence : pas besoin de verrou.

Annulation avec AbortController

let controller
 
async function rechercher(q) {
  controller?.abort()                    // annule la requête précédente
  controller = new AbortController()
  try {
    const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal })
    afficher(await res.json())
  } catch (err) {
    if (err.name !== 'AbortError') throw err
  }
}

Sans cela, une réponse lente à « jav » peut arriver après la réponse à « javascript » et écraser les bons résultats.

Promise.withResolvers

ES2024 simplifie la création de promesses contrôlées de l'extérieur :

const { promise, resolve } = Promise.withResolvers()
bouton.addEventListener('click', resolve, { once: true })
await promise
console.log('Bouton cliqué')

Top-level await

Dans un module ES, await est autorisé en dehors d'une fonction :

config.js
export const config = await fetch('/config.json').then(r => r.json())

Les modules qui importent config.js attendent sa résolution.

Erreurs non gérées

// Node
process.on('unhandledRejection', err => {
  console.error('Promesse rejetée non gérée', err)
  process.exit(1)
})
 
// Navigateur
window.addEventListener('unhandledrejection', e => reportError(e.reason))

Une promesse sans .catch ni try/await qui échoue fait planter les versions récentes de Node. Tant mieux : une erreur silencieuse est pire qu'un crash.

Exercice : retry avec backoff exponentiel

const attendre = ms => new Promise(r => setTimeout(r, ms))
 
async function retry(fn, { tentatives = 4, base = 300 } = {}) {
  for (let i = 0; ; i++) {
    try {
      return await fn()
    } catch (err) {
      if (i >= tentatives - 1) throw err
      const delai = base * 2 ** i + Math.random() * 100
      console.warn(`Tentative ${i + 1} échouée, nouvel essai dans ${Math.round(delai)} ms`)
      await attendre(delai)
    }
  }
}
 
const data = await retry(() => getJSON('https://api.instable.fr/data'))

Le return await dans le try est indispensable : sans await, la promesse serait renvoyée avant d'avoir échoué, et le catch ne verrait jamais l'erreur.


Une seule pile, une file de microtâches vidée entièrement, une file de tâches prise une par une. Tout l'asynchrone JavaScript découle de ce schéma. Quand un comportement vous surprend, redessinez-le : la réponse y est toujours.

Précédent
Le DOM et les événements
Suivant
Modules, npm, outillage et tests

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main