Unit 2 · Software engineering · Lesson 5 of 15
Debugging, code reading, review and refactoring
Builds on
Step 1: Learn the ideas
- Reproducing reliably and minimizing the failing case
- Hypothesis-driven debugging and keeping a log
- Binary search over history, inputs and configuration
- Reading stack traces, logs and traces
- Orienting in an unfamiliar repository
- Code review: what to look for and how to comment
- Characterization tests and safe refactoring steps
- Dependency investigation: changelogs, semantic versioning and lockfiles
- Minimal reproductions for bug reports
- Assertions and invariants as debugging aids
Why it matters
Engineers spend a large part of their working time reading, debugging, reviewing and changing systems they understand only partly, and new team members start almost entirely there. Some interview loops include exercises that look like this work: fix a failing test in a small repository, extend an existing service, review a pull request, or explain what an unfamiliar function does. These rounds reward method over brilliance. An interviewer can see whether you reproduce before guessing, narrow the search deliberately, and leave the code better tested than you found it.
Core ideas
A debugging loop. Reproduce the failure reliably, ideally as an automated test. Minimize it: shrink the input, remove configuration and cut the code path until every remaining part is necessary. Then iterate: study the evidence (error message, logs, stack trace, the most recent change), form one hypothesis, design an experiment whose result could refute it, and run it. Change one thing at a time and write down what you tried; the log prevents loops and makes the final explanation easy. Once fixed, keep the reproduction as a regression test and ask whether the same mistake exists elsewhere. Assertions that check invariants at module boundaries move failures closer to their causes.
Binary search everywhere. If the behavior worked at some point, git bisect halves the commit range at each step. With git bisect run ./check.sh it runs unattended: exit status 0 marks a commit good, 1 to 127 except 125 marks it bad, and 125 skips a commit that cannot be tested. The same idea applies to inputs (drop half and see whether it still fails), to configuration flags and to dependency versions.
Reading a repository. Start with what the project says about itself: the README, build files, CI configuration and directory layout. Build it and run the tests before reading deeply, because a running system answers questions faster than static reading. Pick one concrete flow, such as an HTTP request, a CLI command or a background job, and trace it from entry point to storage with a debugger or temporary logging. Read tests as executable documentation, use git log and git blame to learn why odd code exists, and keep a scratch map of modules and responsibilities.
Code review. Check first whether the change is correct and needed, then its design (does it belong here, does it fit existing abstractions), its tests (would they fail without the change?), and finally naming, readability and documentation. Google's published review guide frames the goal as steadily improving overall code health rather than demanding perfection, asks reviewers to explain their reasoning, and suggests labeling minor points as nits or optional. As an author, keep changes small and describe what changed and why.
Safe refactoring. A refactoring changes structure without changing behavior. Before touching unclear code, pin its current behavior with characterization tests, then move in small named steps from Fowler's catalog (extract function, rename variable, introduce parameter object), running tests after each. Keep refactoring commits separate from behavior changes so review and bisection stay clean.
Dependencies. When an upgrade breaks something, read the changelog and release notes between the two versions, search the project's issue tracker, and bisect across versions if the cause is unclear. Semantic versioning states intent, not a guarantee. Reduce the failure to a minimal reproduction that uses only the dependency; it either reveals your misuse or becomes a high-quality bug report.
Common misconceptions
- "Debugging means reading the code until the bug appears." Static reading scales poorly; experiments that halve the search space scale well.
- "The last thing I changed caused it." Often true, but check whether the failure reproduces on the previous version before committing to that story.
- "Print statements are unprofessional." Targeted logging is a legitimate experiment; what matters is a hypothesis behind each statement and removing them afterwards.
- "Code review is mainly about style." Formatters and linters handle style; reviewers add the most value on correctness, design and missing tests.
- "Refactoring can wait for a rewrite." Large rewrites carry high risk; continuous small refactorings under tests are safer and keep code reviewable.
- "A minor version bump cannot break me." Libraries make mistakes, and code often depends on behavior the authors never promised.
How to approach it
Read the MIT 6.102 debugging reading and the SRE book chapter on effective troubleshooting, then the reviewer and author parts of Google's code review guide. Practice deliberately: plant a bug in a small repository with a few dozen commits and find it with git bisect run; clone an open-source project you use, run its tests and trace one feature end to end; review a real pull request and rank your comments by severity. In interviews, narrate the loop ("I can reproduce it with this input; my hypothesis is X; this experiment separates X from Y") and ask to run code early. Graduate candidates should show method; mid-level candidates should navigate unfamiliar code quickly; senior candidates are expected to debug across services and to shape a team's review and refactoring habits.
Suggested order
- 1Read MIT 6.102 “13: Debugging” and write your own short checklist of the loop: reproduce, minimize, hypothesize, experiment, fix, regression test.
- 2Read the SRE book Ch. 12 (Effective Troubleshooting) for hypothesis-driven investigation of production systems and its common pitfalls.
- 3Read the git-bisect documentation, including “Bisect run”; then build a small repository with 30 or more commits and a planted regression, and find it with a script.
- 4Read Google’s code review guide (“The Standard of Code Review”, “What to look for in a code review”, “How to write code review comments”, “Small CLs”), then review one real open-source pull request and label each comment blocking or optional.
- 5Browse Fowler’s catalog for Extract Function, Rename Variable and Introduce Parameter Object; refactor a messy function of your own in small steps, running tests after each.
- 6Clone an open-source project you depend on, build it, run its tests and trace one feature from entry point to storage; write a half-page map of its modules.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
MIT 6.102 Software Construction (readings)Start here
MIT EECSSection: 13: Debugging
A compact, systematic method: reproduce, find the bug with the scientific method, fix it, and defend against recurrence with assertions and modularity.
Open(opens in a new tab)Link responded, 5 Oct 2026Google Engineering Practices: code review guideStart here
Google (eng-practices documentation)Section: What to look for in a code review
Published guidance for reviewers and authors: the approval standard, what to check, how to write and label comments, and why small changes matter.
Open(opens in a new tab)Link responded, 5 Oct 2026Site Reliability Engineering (Google SRE book)
Google (free online edition)Section: Ch. 12: Effective Troubleshooting
Hypothesis-driven troubleshooting of production systems and pitfalls such as mistaking correlation for causation.
Open(opens in a new tab)Link responded, 5 Oct 2026git-bisect documentation
Git project (git-scm.com)Reference for bisecting history manually or automatically with bisect run and a test script.
Open(opens in a new tab)Link responded, 5 Oct 2026Catalog of Refactorings
refactoring.com (martinfowler.com)Named, behavior-preserving transformations to use as steps when restructuring code under tests.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Do three drills. Bisect: build a small repository with a planted regression and find it with git bisect run, then repeat on a real project’s history. Repository tour: in an open-source project you use, build, run the tests, trace one feature end to end and explain it aloud in five minutes. Review: review a real pull request as you would for a colleague, labeling each comment blocking or optional. For dependencies, upgrade a library across several versions in a scratch project, read the changelog between them and write a minimal reproduction for any behavior change you find.
No external problems are linked; follow the guidance above.
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1What makes a bug reproduction minimal, and why does it matter?
- Question 2What do the exit statuses of the script used with git bisect run mean?
- Question 3How do you find where a given HTTP route is handled in an unfamiliar codebase?
- Question 4Which review comments should block a merge, and which should be suggestions?
- Question 5Why keep refactoring commits separate from behavior changes?
- Question 6What do you check first when a patch-level dependency upgrade breaks a test?
You’re done when you can
- Find a planted regression with git bisect run in a repository you did not write, in under 20 minutes.
- Explain an unfamiliar repository’s main flow after 45 minutes of exploration, with a sketch of its modules.
- Produce a review of a real pull request containing at least one substantive correctness or design comment.
- Refactor a function in at least five small steps with tests passing after each.
- Write a minimal reproduction for a dependency behavior change.


