Les erreurs les plus fréquentes en JavaScript (et comment les éviter)
TypeError, undefined is not a function, closures dans les boucles — les bugs JavaScript que vous allez rencontrer tôt ou tard, expliqués et résolus.
"Cannot read properties of undefined" — si vous avez déjà vu cette erreur à 23h avant une deadline, vous savez de quoi je parle. Ces erreurs reviennent en boucle, peu importe l'expérience. La différence entre un développeur débutant et un développeur senior n'est pas de ne plus les faire — c'est de les identifier en secondes.
1. Cannot read properties of undefined / null
L'erreur la plus fréquente en JavaScript. Vous accédez à une propriété d'une valeur qui est undefined ou null.
// ❌ Erreur
const user = null
console.log(user.name) // TypeError: Cannot read properties of null
const data = undefined
console.log(data.items.length) // TypeError
// ✅ Optional chaining (?.) — disponible depuis ES2020
const user = null
console.log(user?.name) // undefined — pas d'erreur
const data = undefined
console.log(data?.items?.length) // undefined
// ✅ Valeur par défaut avec ??
const name = user?.name ?? 'Anonyme'
// ✅ Guard clause
function processUser(user) {
if (!user) return null // Sortir tôt si null/undefined
return user.name.toUpperCase()
}Avant ES2020, on écrivait user && user.name — fonctionnel mais verbeux. Optional chaining est maintenant supporté partout.
2. this qui ne pointe pas là où vous pensez
this en JavaScript dépend de comment la fonction est appelée, pas de là où elle est définie.
// ❌ Perte du contexte this
const bouton = document.getElementById('btn')
const controller = {
count: 0,
increment() {
this.count++ // ← this est le bouton, pas controller
console.log(this.count)
}
}
bouton.addEventListener('click', controller.increment)
// Résultat : NaN (this.count sur l'élément DOM)
// ✅ Bind explicite
bouton.addEventListener('click', controller.increment.bind(controller))
// ✅ Arrow function (hérite du this parent)
bouton.addEventListener('click', () => controller.increment())
// ✅ Arrow method (si vous contrôlez la définition)
const controller = {
count: 0,
increment: () => { // ⚠️ this sera le this du scope parent, pas controller
// Attention — les arrow functions dans les objets littéraux ne capturent pas l'objet
}
}Règle simple : si vous passez une méthode comme callback, utilisez .bind() ou une arrow function wrapper.
3. Closures dans les boucles
// ❌ Comportement inattendu
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000)
}
// Affiche : 3, 3, 3 — pas 0, 1, 2
// Pourquoi ? var a une portée de fonction, pas de bloc.
// Quand le timeout s'exécute, la boucle est terminée et i = 3
// ✅ let — portée de bloc, une nouvelle variable par itération
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000)
}
// Affiche : 0, 1, 2
// ✅ Ou capturer la valeur dans un IIFE (ancienne technique)
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 1000))(i)
}En 2026, utilisez let et const — oubliez var. var a des comportements de hoisting et de portée qui créent exactement ce genre de bug.
4. Async/await : oublier await
// ❌ Oublier await — la fonction retourne une Promise non résolue
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
return response.json() // ← retourne une Promise, pas les données !
}
async function main() {
const user = fetchUser(42) // ← oublié le await
console.log(user.name) // undefined — user est une Promise
}
// ✅ Correct
async function main() {
const user = await fetchUser(42)
console.log(user.name)
}// ❌ Await dans un forEach — ne fonctionne pas comme attendu
async function processUsers(ids) {
ids.forEach(async (id) => {
await updateUser(id) // Ces promises ne sont pas attendues
})
console.log('Terminé') // S'affiche avant que les updates soient finis
}
// ✅ for...of avec await
async function processUsers(ids) {
for (const id of ids) {
await updateUser(id) // Séquentiel — attend chaque update
}
console.log('Terminé')
}
// ✅ Promise.all si vous voulez le parallèle
async function processUsers(ids) {
await Promise.all(ids.map(id => updateUser(id)))
console.log('Tous terminés')
}5. == vs === (égalité lâche vs stricte)
// == convertit les types avant de comparer
0 == false // true
0 == '' // true
null == undefined // true
1 == '1' // true
[] == false // true
// === compare sans conversion
0 === false // false
1 === '1' // false
null === undefined // false
// Règle : toujours ===, sauf si vous savez exactement ce que vous faites
if (valeur === null || valeur === undefined) { ... }
// Ou plus court :
if (valeur == null) { ... } // null == undefined est acceptable6. Mutation d'objets et de tableaux
// ❌ Mutation inattendue — les objets sont passés par référence
function updateUser(user) {
user.name = 'Alice' // Modifie l'objet original
return user
}
const original = { name: 'William' }
const updated = updateUser(original)
console.log(original.name) // 'Alice' — original muté !
// ✅ Créer une copie
function updateUser(user) {
return { ...user, name: 'Alice' } // Spread — copie superficielle
}
// Tableau
const arr = [1, 2, 3]
arr.push(4) // Mute le tableau original — OK si intentionnel
arr.sort() // Mute aussi
// Copie avant modification
const sorted = [...arr].sort()
const withFour = [...arr, 4]// ❌ Clonage profond incomplet
const deep = { a: { b: 1 } }
const copy = { ...deep }
copy.a.b = 99
console.log(deep.a.b) // 99 — spread est superficiel !
// ✅ Clonage profond
const deep = { a: { b: 1 } }
const copy = structuredClone(deep) // Natif depuis Node 17 et navigateurs modernes
copy.a.b = 99
console.log(deep.a.b) // 1 — original préservé7. Promesses non gérées (unhandled rejection)
// ❌ Promise rejetée sans catch
fetch('/api/data')
.then(res => res.json())
.then(data => processData(data))
// Si une erreur survient → UnhandledPromiseRejection en prod
// ✅ Toujours un catch
fetch('/api/data')
.then(res => res.json())
.then(data => processData(data))
.catch(err => console.error('Erreur:', err))
// ✅ Avec try/catch en async/await
async function loadData() {
try {
const res = await fetch('/api/data')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json()
} catch (err) {
console.error('Erreur:', err)
return null // Valeur par défaut au lieu de planter
}
}8. Comparaisons de références vs valeurs
// ❌ Comparer des objets/tableaux avec ===
const a = { x: 1 }
const b = { x: 1 }
console.log(a === b) // false — deux références différentes
const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3]
console.log(arr1 === arr2) // false
// ✅ Comparer le contenu
console.log(JSON.stringify(a) === JSON.stringify(b)) // true (simple)
// Ou une lib comme lodash isEqual pour les cas complexes
// ✅ Pour les tableaux de primitives
console.log(arr1.join() === arr2.join()) // true9. parseInt sans la base
// ❌ Comportement inattendu avec certains strings
parseInt('08') // 0 en mode octal sur anciens moteurs
parseInt('0x10') // 16 — hexadécimal
// ✅ Toujours spécifier la base
parseInt('08', 10) // 8
parseInt('10', 16) // 16
// Pour convertir un string en nombre entier proprement
Number('42') // 42
+'42' // 42 (unary plus — concis mais moins lisible)
Math.floor(42.9) // 4210. Le piège du NaN
// NaN n'est pas égal à lui-même
console.log(NaN === NaN) // false — c'est le seul cas en JS
// ❌ Tester NaN avec ===
if (valeur === NaN) { ... } // Ne fonctionne jamais
// ✅ Number.isNaN()
if (Number.isNaN(valeur)) { ... } // Correct
// ✅ Ou isNaN() (moins précis — convertit d'abord en nombre)
isNaN('hello') // true — convertit 'hello' en NaN
Number.isNaN('hello') // false — 'hello' n'est pas NaN, c'est un stringLa plupart de ces erreurs disparaissent avec TypeScript : le compilateur attrape les erreurs de type, les propriétés inexistantes et les valeurs potentiellement nulles avant l'exécution. Ce n'est pas que JavaScript est cassé — c'est qu'il fait des choix de design qui priorisent la flexibilité sur la rigueur. Connaître ces pièges, c'est connaître la langue.