Most of my learning is from writing my own code. That means I mostly see my own habits, including the bad ones.
Reading other people's code is the other half of the skill, and the half I practise less. This post is a method I'm trying to turn into a routine: how to pick something to read, how to read it without drowning, and how to turn that into a first contribution. I'm presenting it as a plan, not a record of achievement.
Why reading matters
Writers become better by reading. Programmers are the same, with extra reasons:
- You see choices you wouldn't have made, and sometimes they're better.
- You learn how real projects handle messy cases: errors, versions, odd inputs.
- You get humility. Most mature codebases are less tidy than their READMEs.
- You prepare for the job. Most professional work is reading and changing code that someone else wrote.
What not to do
The classic mistake is opening a large repository and trying to understand all of it. You'll be lost in an hour and quit. Reading code is not like reading a book from page one.
A method that works for me
1. Pick a small library you already use
Familiarity gives you a hook: you know what the library does, so you can read for how. Small matters, since a few hundred lines can be understood completely.
Good candidates: a tiny utility (a debounce helper, an ID generator, a small state library), or a single plugin.
2. Read the README and the tests first
Tests are executable documentation. They show how the author expects the code to be used and which edge cases they cared about. A test file often explains a module faster than the module itself.
3. Find the entry point
Where does the main function live? Look at package.json (main, exports), then open that file.
4. Read one file, not the whole repo
Pick the file that does the core job and read it top to bottom. Ignore everything else for now.
5. Trace one feature end to end
Choose a single behaviour ("what happens when I call debounce(fn, 200)?") and follow it through the code, writing down each hop:
debounce(fn, wait)
-> returns wrapped()
-> wrapped() clears any pending timer
-> sets a new timer for `wait` ms
-> when it fires, calls fn with the *latest* arguments
Writing the trace in your own words is what turns reading into understanding.
6. Ask "why is it structured this way?"
For every surprising decision, form a guess: why a Map instead of an object? Why is this check first? Then look for evidence (a comment, a commit message, an issue). The gap between your guess and the truth is where you learn.
7. Run it and break it
Add a console.log, change a line, see what fails. Reading with a running debugger beats reading alone.
A short worked example
Take a small debounce utility. In about thirty lines you can learn:
function debounce<A extends unknown[]>(fn: (...a: A) => void, wait: number) {
let t: ReturnType<typeof setTimeout> | undefined;
return (...args: A) => {
if (t) clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}
Questions this raises, each a mini-lesson:
- Why is
tdeclared outside the returned function? (It's closure state shared across calls.) - What happens if the component unmounts before the timer fires? (A cleanup/cancel method is missing.)
- Should the first call run immediately (a "leading" option)?
Mature libraries answer those questions in code, which is exactly what you learn by reading them.
Reading code you'll have to change
At work, the goal is usually different: find where to make a change safely. A workflow for that:
- Reproduce the behaviour.
- Search for a distinctive string from the UI or log message.
- Follow it to the function that produces it.
- Read the tests around that function.
- Make the smallest possible change, and run the tests.
From reading to contributing
Reading becomes much more useful once it produces something. Small, realistic first contributions:
- Fix a typo or a confusing sentence in the docs.
- Add a missing test for an edge case you noticed.
- Improve an error message.
- Reproduce a bug someone reported and add what you found.
Open the issue first for anything beyond a typo, read the contributing guide, and keep the change small. A tiny accepted pull request teaches more about a project's workflow than weeks of reading.
The next step I want to take
I want to open a small pull request on a project I use. I haven't made that a regular habit yet, so I'm saying it here to commit. If it happens I'll write up what I learned, including anything that went wrong.
A weekly routine
| Day | Activity |
|---|---|
| Mon | Pick a small library; read the README and tests |
| Tue | Read the core file; write the trace for one feature |
| Wed | Run and break it; note three questions |
| Thu | Look for the answers (issues, commits, docs) |
| Fri | Write five lines of what you learned |
Tools that make reading code easier
You don't have to hold everything in your head. A few commands do a lot.
Find where something is used
rg "transition_transaction" # fast text search (ripgrep)
git grep -n "transition_transaction" # search tracked files
Find when a line or string appeared
git log -S"SKIP LOCKED" --oneline # commits that added or removed that string
git blame -L 40,70 src/claim.ts # who last changed lines 40 to 70, and in which commit
See the history of one file
git log --follow -p -- src/claim.ts
See what a commit changed, and why
git show <commit> # the diff plus the message
Commit messages and linked issues are often where the reasoning lives, which the code alone can't tell you.
How to read a pull request
Reviewing someone's change is reading practice with a built-in goal.
- Read the description first. What is this supposed to do?
- Read the tests. They show intended behaviour.
- Skim the file list. Does the shape of the change match the description?
- Read the core change top-down.
- Ask questions as you read. "What happens if this is empty?"
- Run it if you can.
Practise on open-source projects you use: read recently merged pull requests and see what reviewers pushed back on. That's free mentoring from strangers.
Questions to ask of any unfamiliar code
- What is the input and what is the output?
- Where does state live, and who can change it?
- What can fail, and what happens then?
- What does this depend on?
- What would break if I deleted it?
- Why is there a special case here?
If you can answer all six for a module, you understand it.
Keeping a reading log
A tiny habit with outsized returns:
2026-10-30 library: <name> file: <path>
Learned: <one thing the code did that I wouldn't have thought of>
Question: <something I couldn't explain>
Steal: <a pattern worth reusing>
After a month, the "steal" column is a personal library of patterns, and the "question" column is your study list.
Choosing something to read
| Goal | Pick |
|---|---|
| Learn clean small-scale design | A tiny utility library (a few hundred lines) |
| Learn how frameworks work | A small plugin or example app from the framework's own repository |
| Learn production patterns | Mid-sized open-source apps with good tests |
| Learn algorithms in practice | A scheduler, a parser or a small database |
Start with something you use, so you already have questions.
Common obstacles
"It's too big." Read one file, not the repo.
"I don't understand the language features." Look each one up, then continue. Don't pretend.
"There are no comments." Read the tests, the commit history and the issues.
"I get lost in the indirection." Trace one concrete call with a debugger or print statements instead of reading abstractions.
"I keep forgetting." Write the trace down. Writing is how memory sticks.
One small contribution to aim for
Pick one project you use. Read its issue tracker for tickets labelled "good first issue" or "docs". Find one you can understand in an hour. Leave a comment saying you're taking it, make the smallest change, and run the project's checks before opening the pull request. Being polite and patient matters as much as the code.
What's your favourite codebase to read, and what made it easy to follow?
☕ Enjoyed this post? You can support my writing on Ko-fi.


Top comments (0)