Why was this code written? How to recover the reason behind a change
Git blame tells you who changed a line and when, not why. A practical guide to recovering the reason behind code, and making sure the next person can find it.
The KnowWhyy team · · 6 min read
You find a strange line of code: a retry with a hard-coded delay of 7 seconds, or a check that seems to guard against something that can't happen. You run git blame, and it points you to a commit called "fix" from two years ago, written by someone who has since left. Now you have to decide whether to delete it, and you have no idea what it protects.
This is one of the most common problems in software, and one of the least discussed. This guide covers how to dig out the reason behind a change today, and how to set up your team so the next person doesn't have to dig at all.
The question developers struggle with most
This isn't just you. When Microsoft Research surveyed its own engineers for Software Development at Microsoft Observed, the problem the most developers agreed with was "understanding the rationale behind a piece of code", at 66%. When Thomas LaToza and Brad Myers later asked Microsoft developers for hard-to-answer questions about code, questions about intent and rationale ("why was it written this way?") were the category reported most often.
Part of the reason is that commit messages rarely carry the why. A study of five active open-source projects presented at ICSE 2022 found that, on average, about 44% of commit messages could be improved. The authors define a good message as one that explains both what changed and why. If messages in well-run open-source projects miss the why that often, a busy product team's history is unlikely to do better.
The result is a slow, quiet tax. Every unexplained line becomes a small decision: leave it and carry the confusion forward, or remove it and risk breaking something someone once fixed on purpose.
Ask your first question in minutes.
Create a workspace, connect a tool and choose what gets indexed. The free plan includes GitHub, Slack and Notion.
git blame shows the last commit that touched each line, which is often not the commit that matters. Reformatting, renames and lint fixes bury the real history.
Start by skipping the noise:
# Ignore whitespace changes and follow lines moved or copied between files
git blame -w -C -C -C path/to/file.ts
# Look at the line as it was *before* a given commit
git blame <commit>^ -- path/to/file.ts
If your team has run a big formatter pass, add those commits to a .git-blame-ignore-revs file so nobody has to step around them again:
GitHub's blame view reads this file automatically, so the whole team benefits once it is committed.
Step 2: Trace the line through its whole history
Blame gives you one commit. The story usually spans several. Two git log options are underused here.
# Every commit that changed this function, with diffs
git log -L :retryPayment:src/billing/charge.ts
# Every commit that added or removed a specific string
git log -S "RETRY_DELAY_MS" --oneline
# Same, but matching a regex in the diff
git log -G "retry.*7000" --oneline
git log -L is often the fastest way to see when a piece of logic first appeared and how it changed. The first commit that introduced something is usually more informative than the most recent one that touched it.
Step 3: Follow the trail out of git
The commit is rarely where the reasoning lives. Once you have the right commit, work outward:
Open the pull request. On GitHub, the commit page links to the PR that merged it. The description and review comments often contain the actual discussion: the bug report, the alternatives someone suggested, the reviewer who asked "why not just...?"
Follow the issue link. Most PRs reference an issue or ticket. That is where the original problem was described, often by someone outside engineering.
Search chat for the time window. Take the date of the PR and search your team chat for the feature name, the error message or the customer involved, in the days before it merged. Decisions are often argued in a thread first and only then turned into code. (We cover this in more detail in how to find old decisions in Slack.)
Check the docs from that period. A spec, an incident write-up or a planning doc may explain the constraint that the code is working around.
Ask, in public. If you still can't find it, ask in a team channel rather than a DM, and include what you already found. Someone may remember, and the answer is then searchable for the next person.
This works, but it is slow. Developers in a 2022 study of commit rationale described rationale as having many parts, and often gave up looking for it. The goal of the rest of this guide is to make the trail shorter.
Make the why easy to find next time
Most of the fix is cheap and comes down to habits at the moment of change, when the reason is still in someone's head.
Put a "why" line in your PR template
A template nudges people to write down the reasoning while it is fresh. Keep it short, or people will skip it.
<!-- .github/pull_request_template.md -->
## What changed
## Why
<!-- The problem, the constraint, or the decision this change follows from. -->
## Alternatives considered
<!-- Optional. What you didn't do, and why. -->
## Links
<!-- Issue, Slack thread, spec or incident. -->
Squash-merge so the PR description becomes the commit
If your team squash-merges, configure the repository so the default squash message uses the PR title and description. Then the "Why" section ends up in git log itself, where git blame and git log -L will find it years later, even if the PR is hard to reach.
Write comments that explain why, not what
The code already says what it does. A comment earns its place when it explains something the code can't:
// Bad: retry after 7 seconds
// Good: the payment provider rate-limits retries to one per 5s per card;
// 7s leaves headroom. See incident 2024-03 and PR #812.
await sleep(7000)
Link everything to everything
The most valuable habit is also the simplest: every PR links its issue, every issue links the thread where it was discussed, and every significant thread ends with a link to the PR or doc where the outcome lives. Each link turns a dead end into a path.
A quick reference
You want to know
Try this
Who last touched this line, ignoring formatting
git blame -w -C -C -C
When this logic first appeared
git log -L :function:file
When a constant or string was added or removed
git log -S "string"
The discussion behind a commit
Commit page, then its PR and review comments
The problem that started it
The issue or ticket linked from the PR
The debate before the code
Team chat around the PR date
When to delete the strange code
Sometimes you do all of this and still find nothing. A reasonable rule: if you can't find the reason and the code is cheap to keep, keep it and add a comment saying what you checked. If it is getting in the way, remove it in a small, separate PR whose description says exactly what you searched and what you think it was for, and ask a reviewer who has been around longer. That PR becomes the record for the next person, whichever way it goes. The same idea scales up to bigger decisions, which we cover in how to stop re-litigating old decisions.
Where a tool helps
All of the steps above come down to searching across several places at once: the commit, the PR, the review, the thread and the doc. That is the job KnowWhyy was built for. It connects your GitHub, the Slack channels you choose (direct messages are never read) and Notion. You can then ask in plain language, "why does the payment retry wait 7 seconds?", from the KnowWhyy web app. The answer cites the commit, pull request, thread or page it came from, so you can check it yourself. When the sources don't contain an answer, it says so instead of guessing. See how it works for GitHub history, or try it free with a team of up to 2.
The short version
git blame shows who and when. The why is usually in the PR, the issue or a chat thread.
Skip formatting noise with -w -C and a committed .git-blame-ignore-revs file.
Use git log -L and git log -S to find when the logic first appeared, not just the last touch.
Add a short "Why" section to your PR template and squash-merge so it lands in the commit.
Write comments that explain constraints, and link every PR, issue and thread to each other.
If you remove unexplained code, write down what you checked so the next person doesn't start from zero.