Jour 33 Day 33 · vendredi 18 septembre 2026 Friday 18 September 2026 Qualité Fondamental
Debugger méthodiquement Systematic debugging
Reproduire, réduire, formuler une hypothèse, tester une variable à la fois : la méthode scientifique appliquée aux bugs — et comment raconter votre pire bug en entretien sans partir dans tous les sens. Reproduce, reduce, form a hypothesis, test one variable at a time: the scientific method applied to bugs — and how to tell the story of your hardest bug in an interview without rambling.
L’essentiel
Debugger n’est pas une affaire de talent ou de chance : c’est une méthode scientifique appliquée au code. Le débutant modifie des lignes au hasard jusqu’à ce que « ça marche » ; le développeur méthodique reproduit le bug de façon fiable, réduit le cas au minimum, formule une hypothèse falsifiable, puis mène une expérience en ne changeant qu’une variable à la fois. La différence entre les deux se voit en cinq minutes de pair programming — et les recruteurs le savent : « raconte-moi ton bug le plus difficile » est l’une des questions comportementales les plus fréquentes en entretien de stage.
L’étape zéro, avant toute méthode : lire vraiment le message d’erreur. En entier. Une proportion déprimante de bugs est littéralement expliquée dans le message que l’on a scrollé sans lire.
| Symptôme | Premier réflexe |
|---|---|
| Exception + stack trace | Lire le message en entier, trouver la première frame dans votre code |
| « Ça marchait hier » | Régression → git log des derniers commits, puis git bisect |
| Marche en local, casse en prod | Diff des environnements : versions, variables d’env, config, données |
| Bug intermittent | Suspecter concurrence / timing / données non déterministes ; logger, ne pas « réessayer » |
| Comportement « impossible » | Vérifier que vous exécutez bien le code que vous croyez (build stale, cache, mauvais serveur, mauvaise branche) |
Comment ça marche
La boucle complète tient dans un encadré :
┌──────────────────────────────────────────────┐
│ 1. reproduire (de façon fiable, à volonté) │
│ 2. réduire (cas minimal, par dichotomie) │
│ 3. hypothèse (une seule, falsifiable) │
│ 4. expérience (UNE variable à la fois) │
│ ├─ confirmée → corriger + test │
│ └─ réfutée → retour en 3 │
└──────────────────────────────────────────────┘
- Reproduire d’abord. Un bug qu’on ne sait pas reproduire est un bug qu’on ne saura pas vérifier corrigé. Noter les étapes exactes, les données, l’environnement.
- Réduire ensuite. Supprimer la moitié du code, la moitié des données d’entrée : le bug persiste ? Continuer dans cette moitié. C’est une dichotomie, comme une recherche binaire — on passe de « 3 000 lignes suspectes » à 10 en quelques itérations.
- Une variable à la fois. Si vous changez deux choses et que le bug disparaît, vous ne savez pas laquelle était la cause — et vous avez peut-être introduit un second bug qui masque le premier. Chaque expérience doit pouvoir réfuter l’hypothèse.
- Corriger, puis verrouiller : un test de non-régression qui échoue sans le fix et passe avec. Sinon le bug reviendra, et personne ne s’en apercevra avant la prod.
Pour les régressions (« ça marchait avant »), la dichotomie a un outil dédié : git bisect, qui fait une recherche binaire dans l’historique. 1 000 commits suspects = ~10 étapes, pas 1 000.
# La feature marchait en v2.3.0, cassée sur main : régression.
git bisect start
git bisect bad # HEAD est cassé
git bisect good v2.3.0 # ce tag marchait
# → git checkout un commit à mi-chemin ; on teste :
npm test
git bisect good # ou "bad" selon le résultat
# ... ~log2(N) itérations, puis :
# "abc1234 is the first bad commit"
# Version automatisée : exit code 0 = good, autre = bad
git bisect run npm test
git bisect reset # revenir où on était
💡 Le bug est dans TON code — statistiquement, le compilateur, le framework et la lib à 40 millions de téléchargements hebdomadaires ne sont pas cassés. « J’ai trouvé un bug dans React » est possible, mais c’est l’hypothèse à tester en dernier, après avoir éliminé tout votre code. Ce réflexe d’humilité fait gagner des heures — et il s’entend très bien en entretien.
Concepts clés à maîtriser
- Lire une stack trace : identifier le message (le quoi), puis descendre jusqu’à la première frame qui appartient à votre code (le où). Les frames du framework autour sont du contexte, pas des suspects. Attention : certaines stacks listent l’appel le plus profond en haut (Python : en bas).
- Debugger vs printf : un debugger permet de poser un breakpoint (y compris conditionnel :
i == 4217), d’inspecter tout l’état sans redéployer, de step over/into ligne par ligne et de poser des watch sur des expressions. Leprintf/console.loggarde deux avantages : il capture une chronologie (précieux pour l’asynchrone) et il marche en prod — les logs sont le debugger de la production. Les deux sont des outils légitimes ; savoir dire quand utiliser lequel est le vrai signal senior. - Heisenbugs : un bug qui disparaît sous debugger ou dès qu’on ajoute un
printest presque toujours un problème de timing — race condition, deadlock évité par le ralentissement, mémoire non initialisée. Le debugger modifie l’expérience : il fige les threads, change l’ordonnancement. Réflexe : logging léger + horodaté, thread sanitizer, relire les sections critiques. - Rubber duck debugging : expliquer le problème à voix haute, ligne par ligne, à un canard en plastique (ou un collègue silencieux). Ça marche parce que verbaliser force à vérifier chaque hypothèse implicite — et c’est en général l’une d’elles qui est fausse. La moitié des questions posées à un senior se résolvent pendant qu’on les formule.
- Poser une bonne question : un cas minimal reproductible (le code le plus court qui montre le bug), ce que vous attendiez, ce qui se passe, ce que vous avez déjà essayé, versions et environnement. Construire ce cas minimal résout le problème une fois sur deux ; les autres fois, vous obtenez une réponse en minutes au lieu de jours.
⚠️ Le shotgun debugging — modifier des lignes au hasard jusqu’à ce que le symptôme disparaisse. Même quand « ça marche », vous n’avez rien appris, vous avez probablement masqué la cause racine, et le bug reviendra sous une autre forme. Un fix dont on ne peut pas expliquer pourquoi il fonctionne n’est pas un fix.
En entretien
« Raconte-moi le bug le plus difficile que tu aies résolu. » — Structurez : ① contexte en une phrase, ② symptôme observable, ③ démarche (hypothèses successives, outils utilisés, fausses pistes assumées), ④ cause racine, ⑤ fix + ce que vous avez mis en place pour qu’il ne revienne pas. Le recruteur évalue votre méthode, pas la difficulté du bug — un bug simple raconté avec une démarche limpide vaut mieux qu’un bug épique raconté en vrac.
« Un bug apparaît en prod mais pas en local, tu commences par quoi ? » — Par les différences : versions (runtime, dépendances), variables d’environnement, config, données réelles vs données de test, charge/concurrence. Puis les logs de prod autour de l’incident. Le bug vit forcément dans un des deltas.
« C’est quoi git bisect ? » — Une recherche binaire dans l’historique Git pour trouver le commit qui a introduit une régression : on donne un commit good et un commit bad, Git checkout le milieu, on teste, on répond good/bad, et on converge en O(log n). Bonus : git bisect run <cmd> automatise tout si un test reproduit le bug.
« Debugger ou console.log ? » — Les deux, selon le contexte : debugger pour explorer un état complexe à un instant T (breakpoints conditionnels, watch, step) ; logs pour les chronologies asynchrones, les bugs intermittents et la prod. Répondre « uniquement l’un des deux » est un drapeau rouge.
« Un bug disparaît quand tu ajoutes un print, qu’est-ce que ça t’évoque ? » — Une race condition (ou un problème de timing) : le print ralentit le thread et change l’ordonnancement. C’est un indice, pas une solution — le bug est toujours là, il attend la prod.
Pièges & idées reçues
- Corriger le symptôme, pas la cause : attraper l’exception et continuer, ajouter un
if nullsans comprendre pourquoi c’est null. Le bug se déplace, il ne disparaît pas. - « Impossible, ce code n’a pas changé » — mais l’environnement, les données, une dépendance ou l’horloge ont changé. Un code inchangé dans un monde qui change peut casser.
- Googler le nom générique de l’exception (
NullPointerException) au lieu de votre message complet avec son contexte. Le message précis est votre meilleure requête. - S’acharner seul pendant des heures : au-delà de 30-45 minutes sans progrès, canard en plastique, pause, ou question bien posée à un humain. La ténacité, c’est de la méthode, pas de l’isolement.
- Ne pas écrire le test de non-régression après le fix : le même bug reviendra au prochain refactor, et il aura coûté deux fois.
🎤 En entretien — préparez à l’avance deux histoires de bugs (une technique, une « détective ») en suivant la structure symptôme → démarche → cause → fix → prévention. C’est une question quasi certaine, et l’improvisation se voit. Mentionner une fausse piste assumée (« j’ai d’abord cru à X, l’expérience l’a réfuté ») rend le récit crédible et montre la méthode.
Pour aller plus loin
- A debugging manifesto — Julia Evans et sa zine The Pocket Guide to Debugging
- git bisect — documentation officielle, avec la section
bisect run - How to ask — Stack Overflow et Minimal reproducible example : la checklist d’une bonne question
- Debugging: The 9 Indispensable Rules — David J. Agans : court, ancien, toujours juste
The essentials
Debugging is not about talent or luck: it’s the scientific method applied to code. The beginner changes random lines until “it works”; the methodical developer reproduces the bug reliably, reduces the case to a minimum, forms one falsifiable hypothesis, then runs an experiment changing only one variable at a time. The difference between the two shows within five minutes of pair programming — and interviewers know it: “tell me about your hardest bug” is one of the most common behavioral questions in internship interviews.
Step zero, before any method: actually read the error message. All of it. A depressing share of bugs is literally explained in the message you scrolled past without reading.
| Symptom | First reflex |
|---|---|
| Exception + stack trace | Read the message in full, find the first frame in your code |
| “It worked yesterday” | Regression → git log of recent commits, then git bisect |
| Works locally, breaks in prod | Diff the environments: versions, env vars, config, data |
| Intermittent bug | Suspect concurrency / timing / non-deterministic data; log it, don’t just “retry” |
| “Impossible” behavior | Check you’re running the code you think you are (stale build, cache, wrong server, wrong branch) |
How it works
The whole loop fits in one box:
┌──────────────────────────────────────────────┐
│ 1. reproduce (reliably, on demand) │
│ 2. reduce (minimal case, by bisection) │
│ 3. hypothesis (a single, falsifiable one) │
│ 4. experiment (ONE variable at a time) │
│ ├─ confirmed → fix + test │
│ └─ refuted → back to 3 │
└──────────────────────────────────────────────┘
- Reproduce first. A bug you can’t reproduce is a bug you can’t verify as fixed. Write down the exact steps, the data, the environment.
- Then reduce. Delete half the code, half the input data: bug still there? Keep going in that half. It’s a bisection, like binary search — you go from “3,000 suspicious lines” to 10 in a few iterations.
- One variable at a time. If you change two things and the bug disappears, you don’t know which one was the cause — and you may have introduced a second bug masking the first. Every experiment must be able to refute the hypothesis.
- Fix, then lock it in: a regression test that fails without the fix and passes with it. Otherwise the bug will come back, and nobody will notice before production.
For regressions (“it used to work”), bisection has a dedicated tool: git bisect, a binary search through history. 1,000 suspect commits = ~10 steps, not 1,000.
# The feature worked in v2.3.0, broken on main: a regression.
git bisect start
git bisect bad # HEAD is broken
git bisect good v2.3.0 # this tag worked
# → git checks out a commit halfway; we test:
npm test
git bisect good # or "bad" depending on the result
# ... ~log2(N) iterations, then:
# "abc1234 is the first bad commit"
# Automated version: exit code 0 = good, anything else = bad
git bisect run npm test
git bisect reset # go back to where you were
💡 The bug is in YOUR code — statistically, the compiler, the framework and the library with 40 million weekly downloads are not broken. “I found a bug in React” is possible, but it’s the hypothesis to test last, after eliminating all of your own code. This reflex of humility saves hours — and it sounds very good in an interview.
Key concepts to master
- Reading a stack trace: identify the message (the what), then walk down to the first frame that belongs to your code (the where). The framework frames around it are context, not suspects. Careful: some stacks list the deepest call at the top (Python: at the bottom).
- Debugger vs printf: a debugger lets you set a breakpoint (including conditional ones:
i == 4217), inspect all state without redeploying, step over/into line by line and set watches on expressions.printf/console.logkeeps two advantages: it captures a timeline (precious for async code) and it works in prod — logs are production’s debugger. Both are legitimate tools; knowing when to use which is the real senior signal. - Heisenbugs: a bug that disappears under a debugger or as soon as you add a
printis almost always a timing problem — race condition, deadlock avoided by the slowdown, uninitialized memory. The debugger changes the experiment: it freezes threads, alters scheduling. Reflex: lightweight timestamped logging, thread sanitizer, re-read the critical sections. - Rubber duck debugging: explain the problem out loud, line by line, to a rubber duck (or a silent colleague). It works because verbalizing forces you to check every implicit assumption — and one of them is usually the false one. Half the questions asked of a senior get solved while being phrased.
- Asking a good question: a minimal reproducible example (the shortest code that shows the bug), what you expected, what happens, what you already tried, versions and environment. Building that minimal case solves the problem half the time; the other half, you get an answer in minutes instead of days.
⚠️ Shotgun debugging — changing random lines until the symptom disappears. Even when “it works”, you’ve learned nothing, you’ve probably masked the root cause, and the bug will come back in another shape. A fix you can’t explain the why of is not a fix.
In an interview
“Tell me about the hardest bug you’ve solved.” — Structure it: ① context in one sentence, ② observable symptom, ③ approach (successive hypotheses, tools used, dead ends you own up to), ④ root cause, ⑤ fix + what you put in place so it can’t come back. The interviewer is evaluating your method, not the bug’s difficulty — a simple bug told with a crisp approach beats an epic bug told as a jumble.
“A bug shows up in prod but not locally, where do you start?” — With the differences: versions (runtime, dependencies), environment variables, config, real data vs test data, load/concurrency. Then the prod logs around the incident. The bug necessarily lives in one of the deltas.
“What is git bisect?” — A binary search through Git history to find the commit that introduced a regression: you give a good commit and a bad commit, Git checks out the midpoint, you test, answer good/bad, and it converges in O(log n). Bonus: git bisect run <cmd> automates the whole thing if a test reproduces the bug.
“Debugger or console.log?” — Both, depending on context: debugger to explore complex state at a point in time (conditional breakpoints, watch, step); logs for async timelines, intermittent bugs and production. Answering “only one of the two” is a red flag.
“A bug disappears when you add a print, what does that suggest?” — A race condition (or a timing issue): the print slows the thread down and changes scheduling. It’s a clue, not a solution — the bug is still there, waiting for production.
Pitfalls & misconceptions
- Fixing the symptom, not the cause: catching the exception and moving on, adding an
if nullwithout understanding why it’s null. The bug moves, it doesn’t disappear. - “Impossible, this code hasn’t changed” — but the environment, the data, a dependency or the clock has. Unchanged code in a changing world can break.
- Googling the generic exception name (
NullPointerException) instead of your full message with its context. The precise message is your best query. - Grinding alone for hours: past 30-45 minutes without progress, rubber duck, break, or a well-formed question to a human. Tenacity is method, not isolation.
- Skipping the regression test after the fix: the same bug will return at the next refactor, having cost you twice.
🎤 In an interview — prepare two bug stories in advance (one technical, one “detective work”) following the structure symptom → approach → cause → fix → prevention. This question is near-certain, and improvisation shows. Mentioning an owned dead end (“I first suspected X, the experiment refuted it”) makes the story credible and shows the method.
Going further
- A debugging manifesto — Julia Evans and her zine The Pocket Guide to Debugging
- git bisect — official documentation, including the
bisect runsection - How to ask — Stack Overflow and Minimal reproducible example: the checklist of a good question
- Debugging: The 9 Indispensable Rules — David J. Agans: short, old, still right