ITSkillsCenter
Blog

JavaScript moderne : patterns pratiques pour code propre

12 min de lecture

Lecture : 12 minutes · Niveau : intermédiaire · Mise à jour : avril 2026

JavaScript moderne (ES2020+) offre des outils qui rendent le code plus lisible, plus sûr, et plus expressif que le JavaScript des années 2010. Mais maîtriser la syntaxe ne suffit pas : il faut connaître les patterns qui exploitent réellement ces fonctionnalités. Ce guide rassemble les patterns vraiment utiles pour produire du code propre en équipe, sans tomber dans la sophistication gratuite ni dans le code qui se croit malin mais devient illisible pour les collègues.

L’écriture de bon JavaScript moderne ne consiste pas à utiliser le maximum de fonctionnalités récentes, mais à choisir la forme la plus claire pour chaque situation. Un excellent ingénieur écrit souvent du code qui paraît simple : c’est précisément cette simplicité, atteinte en connaissant les patterns appropriés, qui rend le code maintenable sur la durée par des personnes qui n’étaient pas là lors de son écriture.

Voir aussi → TypeScript et JavaScript moderne : guide pratique.


Sommaire

  1. Async/await : faire mieux que .then()
  2. Promesses parallèles avec Promise.all et amis
  3. Optional chaining et nullish coalescing
  4. Déstructuration avancée
  5. Spread et rest pour l’immutabilité
  6. Itérables, générateurs, et for...of
  7. Gestion d’erreurs propre
  8. Modules ES et organisation du code
  9. Patterns à éviter en 2026
  10. FAQ

1. Async/await : faire mieux que .then()

// L'ancienne manière, illisible
function chargerProfil(id) {
  return fetch(`/api/users/${id}`)
    .then(r => {
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      return r.json();
    })
    .then(user => {
      return fetch(`/api/users/${user.id}/posts`)
        .then(r => r.json())
        .then(posts => ({ user, posts }));
    });
}

// Async/await, lisible
async function chargerProfil(id) {
  const r = await fetch(`/api/users/${id}`);
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  const user = await r.json();

  const rPosts = await fetch(`/api/users/${user.id}/posts`);
  const posts = await rPosts.json();

  return { user, posts };
}

async/await rend le code asynchrone aussi lisible que du synchrone. Toujours préférer cette forme aux chaînes .then(), sauf cas particulier (composition fonctionnelle de promesses).

Erreurs avec async/await

async function safeLoad(id) {
  try {
    return await chargerProfil(id);
  } catch (err) {
    console.error("Échec chargement:", err);
    return null;
  }
}

Le try/catch autour d’un await capture les rejets. C’est bien plus lisible que les .catch() chaînés.


2. Promesses parallèles avec Promise.all et amis

Quand plusieurs requêtes sont indépendantes, les lancer en parallèle au lieu de séquentiellement.

// Mauvais : 3 requêtes en série, 3x plus lent
async function dashboard() {
  const stats = await fetch("/api/stats").then(r => r.json());
  const ventes = await fetch("/api/ventes").then(r => r.json());
  const clients = await fetch("/api/clients").then(r => r.json());
  return { stats, ventes, clients };
}

// Bon : 3 requêtes en parallèle
async function dashboard() {
  const [stats, ventes, clients] = await Promise.all([
    fetch("/api/stats").then(r => r.json()),
    fetch("/api/ventes").then(r => r.json()),
    fetch("/api/clients").then(r => r.json()),
  ]);
  return { stats, ventes, clients };
}

Variantes

  • Promise.all([...]) : attend toutes, échoue si une échoue
  • Promise.allSettled([...]) : attend toutes, ne plante jamais, retourne le statut de chaque
  • Promise.race([...]) : la première qui termine (succès ou échec) gagne
  • Promise.any([...]) : la première qui réussit gagne, n’échoue que si toutes échouent

allSettled est précieux quand on veut afficher partiellement des résultats même si certaines requêtes échouent.

const resultats = await Promise.allSettled([
  fetch("/api/source1").then(r => r.json()),
  fetch("/api/source2").then(r => r.json()),
  fetch("/api/source3").then(r => r.json()),
]);

const ok = resultats.filter(r => r.status === "fulfilled").map(r => r.value);
const ko = resultats.filter(r => r.status === "rejected").map(r => r.reason);

3. Optional chaining et nullish coalescing

// Sans optional chaining, défensif et verbeux
const ville = utilisateur && utilisateur.adresse && utilisateur.adresse.ville;

// Avec optional chaining
const ville = utilisateur?.adresse?.ville;

// Avec valeur par défaut
const ville = utilisateur?.adresse?.ville ?? "Inconnue";

// Sur une méthode qui peut ne pas exister
service.optionnelMethod?.();

// Sur un index de tableau
const premier = liste?.[0];

?. court-circuite proprement. ?? ne se déclenche que pour null ou undefined, contrairement à || qui se déclenche aussi pour 0, "", false.

const port = config.port || 3000;        // si port = 0, prend 3000 (souvent indésirable)
const port = config.port ?? 3000;        // si port = 0, garde 0 (correct)

4. Déstructuration avancée

Avec renommage et défauts

const { nom, email: courriel, role = "user" } = utilisateur;
// nom et courriel sont extraits, role par défaut "user" si absent

Imbriquée

const { adresse: { ville, codePostal } } = utilisateur;

Attention : si adresse est undefined, ça plante. Avec optional chaining intermédiaire :

const ville = utilisateur?.adresse?.ville;

Dans les paramètres de fonction

function envoyerEmail({ to, sujet, corps, cc = [] }) {
  // ...
}
envoyerEmail({ to: "client@x.com", sujet: "Hello", corps: "..." });

Plus lisible que envoyerEmail("client@x.com", "Hello", "...") quand il y a plusieurs paramètres, et permet d’ajouter des paramètres sans casser les appels existants.

Tableaux

const [premier, deuxieme, ...reste] = liste;
const [, , troisieme] = liste;  // ignore les deux premiers

5. Spread et rest pour l’immutabilité

// Cloner un objet (shallow)
const copie = { ...original };

// Fusionner avec overrides
const config = { ...defauts, ...userConfig };

// Ajouter une propriété sans muter
const updated = { ...original, status: "actif" };

// Cloner un tableau
const copie = [...original];

// Concaténer
const fusion = [...arr1, ...arr2];

// Ajouter en tête ou queue
const avec = [premier, ...arr];
const avec = [...arr, dernier];

L’immutabilité est précieuse pour la prévisibilité du code (notamment dans React, Redux, Vuex). Rester sur des transformations qui créent de nouvelles valeurs au lieu de muter les anciennes simplifie le débogage.

Attention : shallow clone seulement

const original = { user: { nom: "Acme" } };
const copie = { ...original };
copie.user.nom = "Modifié";  // modifie aussi original.user.nom !

Pour des structures profondes : structuredClone(original) (intégré ES2022, fonctionne sur la plupart des objets), ou JSON.parse(JSON.stringify(original)) pour des données simples.


6. Itérables, générateurs, et for...of

// Itérer sur les valeurs d'un tableau (préférer à .forEach pour async)
for (const item of liste) {
  await traiter(item);
}

// Itérer avec index
for (const [i, item] of liste.entries()) {
  console.log(i, item);
}

// Itérer sur les entrées d'un objet
for (const [cle, valeur] of Object.entries(obj)) {
  console.log(cle, valeur);
}

// Itérer sur un Map / Set
for (const [k, v] of monMap) { /* ... */ }
for (const v of monSet) { /* ... */ }

Générateurs pour streams

function* compter(jusqu) {
  for (let i = 0; i < jusqu; i++) {
    yield i;
  }
}

for (const n of compter(1_000_000)) {
  // n produit à la demande, pas tout en mémoire
  if (n > 100) break;
}

Utile pour des sources potentiellement infinies ou très volumineuses.

Async iteration

// Lire un stream ligne par ligne
async function* lireLignes(stream) {
  let buffer = "";
  for await (const chunk of stream) {
    buffer += chunk;
    const lignes = buffer.split("\n");
    buffer = lignes.pop();
    for (const ligne of lignes) yield ligne;
  }
  if (buffer) yield buffer;
}

for await (const ligne of lireLignes(file)) {
  console.log(ligne);
}

7. Gestion d’erreurs propre

Classes d’erreur custom

class ValidationError extends Error {
  constructor(message, champ) {
    super(message);
    this.name = "ValidationError";
    this.champ = champ;
  }
}

throw new ValidationError("Email invalide", "email");

// Capture spécifique
try {
  valider(data);
} catch (err) {
  if (err instanceof ValidationError) {
    afficher(`${err.champ}: ${err.message}`);
  } else {
    throw err;  // re-throw pour autres erreurs
  }
}

Différencier les types d’erreurs permet une gestion adaptée à chaque cas plutôt qu’un catch-all générique.

Pattern Result/Either

Pour des contextes où on préfère retourner les erreurs plutôt que les jeter :

async function valider(input) {
  if (!input.email) {
    return { ok: false, error: "Email requis" };
  }
  return { ok: true, value: input };
}

const r = await valider(data);
if (!r.ok) {
  console.error(r.error);
  return;
}
// r.value est garanti utilisable ici

Bibliothèques comme neverthrow ou fp-ts formalisent ce pattern.

Cause des erreurs (ES2022)

try {
  await loadConfig();
} catch (err) {
  throw new Error("Init failed", { cause: err });
}

cause préserve la stack d’origine, précieux pour le debug en production.


8. Modules ES et organisation du code

Imports nommés vs défaut

// Préférer les exports nommés
export function valider(...) { ... }
export const VERSION = "1.0";

// Au lieu de default
export default function valider(...) { ... }

Les exports nommés permettent l’auto-import dans l’IDE, le tree-shaking optimal, et évitent les confusions de nom à l’import.

Re-exports pour barrels

src/services/index.js :

export { ClientService } from "./client.js";
export { ProductService } from "./product.js";
export * from "./types.js";

Permet d’importer plus simplement :

import { ClientService, ProductService } from "@/services";

À utiliser avec parcimonie : sur des gros projets, des barrels mal pensés ralentissent le bundling.

Lazy loading

// Charger une fonctionnalité lourde à la demande
async function ouvrirEditor() {
  const { Editor } = await import("./editor.js");
  return new Editor();
}

Réduit la taille du bundle initial, améliore le temps de chargement.


9. Patterns à éviter en 2026

var

À bannir totalement. let ou const partout, avec préférence à const quand possible.

== (égalité non stricte)

0 == "" // true (surprenant)
null == undefined // true
"" == false // true

Toujours === qui ne fait pas de conversion implicite.

arguments dans les fonctions

// Mauvais
function add() {
  return Array.from(arguments).reduce((a, b) => a + b);
}

// Bon
function add(...nombres) {
  return nombres.reduce((a, b) => a + b);
}

for...in sur des tableaux

// Mauvais : for...in sur un tableau
for (const i in arr) { ... }  // i est string, ordre non garanti

// Bon
for (const item of arr) { ... }
for (let i = 0; i < arr.length; i++) { ... }

IIFE pour scope (obsolète avec modules)

// L'ancienne pratique
(function() {
  // code isolé
})();

// Avec modules ES, chaque fichier a son propre scope
// IIFE inutile

Voir aussi → TypeScript et JavaScript moderne : guide pratique pour ajouter le typage à ces patterns.


10. FAQ

Async/await partout, même pour des opérations simples ?

Si la fonction fait au moins une opération asynchrone : oui. Pour une fonction purement synchrone, async ajoute une promesse pour rien. Mais async sur une fonction qui orchestre plusieurs awaits reste préférable même si certaines branches sont sync.

Faut-il systématiquement utiliser Promise.all ?

Non. Si les requêtes dépendent l’une de l’autre (la deuxième a besoin du résultat de la première), il faut séquentiel. Promise.all est pour des opérations vraiment indépendantes.

forEach ou for...of pour itérer ?

for...of est préférable dans la plupart des cas, surtout avec async/await (forEach ne respecte pas async). forEach reste valide pour des transformations simples synchrones.

Optional chaining sur un tableau au lieu de vérifier la longueur ?

const premier = liste?.[0]; // safe même si liste undefined

Oui, mais attention à la sémantique : liste?.[0] retourne undefined si liste est null OU si l’index 0 n’existe pas. Si la distinction matter, faire deux vérifications séparées.

Comment éviter le callback hell sans async/await ?

Avec Promise.all pour parallèle, et chaînage .then() linéaire. Mais async/await reste la solution la plus lisible. Pas de raison de ne pas l’utiliser en 2026.

JSON.parse(JSON.stringify(x)) ou structuredClone(x) ?

structuredClone (ES2022) gère plus de types : Map, Set, Date, RegExp, ArrayBuffer, etc. JSON ne gère que des types JSON-sérialisables. structuredClone est aussi plus rapide. À préférer sauf compatibilité avec runtimes très anciens.

Modules ES dans Node.js : .mjs ou type:module ?

type: "module" dans package.json rend tous les .js ESM. Plus pratique. Les .mjs restent utiles pour quelques fichiers ESM dans un projet majoritairement CommonJS, ou inversement .cjs dans un projet majoritairement ESM.


Articles liés (cluster TypeScript et JavaScript moderne)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité