Nguyen Le PhongNguyen Le Phong

Reading Code Is Harder Than Writing It

A reflective essay on why reading code often feels harder than writing it, and how patient tracing, tests, naming, history, and small notes help engineers understand a codebase without rushing to rewrite it.

The cursor is sitting on a function that looks innocent. It takes a user, a plan, and a date. The name suggests one thing, the tests suggest another, and a comment from two years ago mentions an enterprise exception nobody remembers. You have not written a line yet, but you are already tired. Reading code can feel like entering a room after a long meeting has ended and trying to infer the whole conversation from the whiteboard.

Writing code has a small private comfort: you begin with your own intention. Even when the problem is hard, the first shape comes from your head. Reading code starts differently. The intention belongs to someone else, sometimes to several people across several years. You are not only reading syntax. You are reading decisions, deadlines, incidents, naming habits, old constraints, and the quiet compromises that never made it into documentation.

This is why experienced engineers can feel slow in an unfamiliar codebase. It is not because they forgot how to code. It is because understanding is heavier than typing. A new feature may require one file change, but knowing which file deserves the change may require tracing routes, checking tests, reading database migrations, searching old pull requests, and asking why a strange branch exists. The work is invisible until it prevents the wrong edit.

Code also hides time. A conditional may look strange until you learn it was added after a customer incident. A duplicated helper may look careless until you learn the two flows are slowly diverging. A missing abstraction may be a wise choice because the team tried the abstraction before and paid for it. Without history, every line invites judgment. With history, many lines become evidence of a trade-off.

The hardest habit is resisting the urge to rewrite too early. Rewriting gives immediate relief because your own structure is easier for you to understand. But that relief can be expensive if you have not yet understood the behavior the old code protects. Legacy code is often carrying small business rules in its pockets. A branch that looks redundant may be serving one old contract. A weird null check may be preventing a production bug that only appears with imported data from one partner.

Tests help because they turn reading into a conversation with evidence. A good test shows what the code promises, not only how it happens to be implemented. When tests are missing, a small characterization test can be an act of humility. It says: before I improve this, I want to capture what it currently does. That does not make bad code good. It gives the next change a safer floor.

Tracing one path end to end is often better than trying to understand everything at once. Start with one user action, one API request, one event, or one failing test. Follow it through the codebase. Write down the names of the functions, tables, queues, and services it touches. Notice where data changes shape. Notice where errors are swallowed. Notice where logs appear or disappear. A codebase becomes less frightening when it turns from a forest into a path.

Names matter more when people are reading than when one person is writing. A vague name makes every future reader pay a small tax. A precise name can remove a paragraph of explanation. This does not mean every function needs a long title. It means the name should carry the distinction the reader needs: draft versus submitted, calculated versus stored, display price versus billing price, tenant permission versus user permission. Good names are small acts of care for people who arrive later.

Comments are similar. The useful comment rarely explains what the code already says. It explains why this shape exists, why the obvious alternative was avoided, or what external constraint is being respected. A short note like partner import sends empty strings for missing dates can save someone from cleaning up a line that is quietly doing real work. The best comments make future reading less theatrical.

Reading code also becomes easier in teams that treat questions as progress. When someone asks why a module is shaped a certain way, the answer should not be a test of whether they are senior enough. It is a chance to restore shared memory. Many codebases become hard not because the code is uniquely complex, but because the context has been allowed to live only in a few people's heads.

There is a patient kind of engineering hidden here. Before changing code, learn its job. Before judging a strange line, find the pressure that created it. Before adding a new abstraction, understand the duplication it is trying to replace. This patience is not slowness. It is how a team avoids turning every improvement into a new layer of confusion.

The next time reading code feels harder than writing it, it may help to treat that difficulty as real work, not as a personal weakness. Trace one path. Add one note. Capture one behavior in a test. Ask one careful question. Understanding grows in small passes, and a codebase becomes kinder every time someone leaves behind a little more context than they found.

Qu'en avez-vous pensé ?