Reading code before writing it
Every change starts with a question: where does this behavior actually live? Reading is the skill that makes the answer cheap.
- You have a repeatable method for tracing a feature through code you've never seen.
- You know the four shortcuts that replace "read everything" — and why reading tests beats reading code.
New engineers often try to read a codebase like a book, front to back. It doesn't work: real systems are not linear, and most of the code is irrelevant to your question. The efficient method is the opposite — start at the edge the user touches, and follow one path until you can point at the line that decides the behavior you care about.
4.1Trace it, step by step
Search the code for the route string: /orders/:id. In most stacks that lands in one file with every route, or next to the handler. Resist opening anything else yet.
Shortcut: search for a user-visible string from the UI ("Order total") — it often appears in both the template and the code that builds it.
The handler parses input, checks auth, and calls something. That something is where the interesting logic lives — note its name and follow it.
Here it is: a service method with the actual conditions. if (order.status !== "paid") throw .... This is the line that decides the behavior you're chasing.
Read the inputs and the early returns first; they're where assumptions hide.
The repository returns a row, mapped to a domain object somewhere. Mismatches between database columns and code fields are a classic source of "impossible" bugs.
The tests are the specification nobody keeps in sync by hand: which cases are covered, which throw, what the fixture looks like. They are usually faster to read than the code.
Then git log -p on that file: the commit message tells you why the check exists — usually a bug from two years ago. Chesterton's fence: don't remove it until you know what it was holding back.
4.2Four shortcuts worth memorising
- Search the error, not the concept. Paste the exact message from the logs into the code search — it lands you within one file.
- Follow the type, not the file. "Go to definition" on a parameter is faster than browsing folders, and it shows every caller.
- Read the tests first. They show the intended behavior in the smallest possible example, including edge cases nobody documented.
- Stop time with the debugger. A breakpoint in the handler lets you inspect the real object graph instead of imagining it.
4.3Leave it slightly better
You now understand one path through the code better than anyone did an hour ago. That knowledge decays fast, so spend it immediately and in small amounts: rename the variable that lies about what it holds, add the test that would have caught the bug you just fixed, put one sentence in the README about the behavior you had to reverse-engineer.
This is the only sustainable form of cleanup. Grand rewrites fail because they require understanding everything before changing anything — the opposite of how understanding actually arrives. One honest improvement per visit, a hundred visits, and a codebase becomes pleasant to work in without anyone scheduling a project for it.
The code looks wrong, so you replace it — and the bug it was guarding against comes back three weeks later. That odd check might be load-bearing. Find the commit that added it before you delete it; if there isn't one, ask the person who did.
You must change an unfamiliar feature. What should you read first?
60-second recap
- Start at the edge, follow one path. Reading the whole tree is not a strategy.
- The rule lives in the service, the assumptions live in the handler, the truth lives in the tests.
- Search exact strings from the UI or the logs; use go-to-definition; stop time with a debugger.
- Check history before deleting weird code. Someone wrote that check for a reason.