// Add inline comments at decision points that explain the "why" behind non-obvious code using knowable information from names, tests, and history. Use when: (1) committing code with non-obvious decisions where future readers will ask "why this way" when reading the implementation, (2) code appears wrong or overly complex but tests or history reveal deliberate constraints, (3) patterns violate project conventions and inline explanation would distinguish intentional deviation from technical debt, (4) pull request comments contain explanations that would be more valuable preserved directly in the code
| name | expression-annotate |
| description | Add inline comments at decision points that explain the "why" behind non-obvious code using knowable information from names, tests, and history. Use when: (1) committing code with non-obvious decisions where future readers will ask "why this way" when reading the implementation, (2) code appears wrong or overly complex but tests or history reveal deliberate constraints, (3) patterns violate project conventions and inline explanation would distinguish intentional deviation from technical debt, (4) pull request comments contain explanations that would be more valuable preserved directly in the code |
Add comments that explain why code exists in its current form, drawing from information that can be known or inferred from the codebase itself.
When I need to make code comprehensible beyond what the implementation reveals, I add comments that capture the reasoning, constraints, and context that led to current decisions, understanding that future readers will have the code but not the conversations, attempts, and discoveries that shaped it.
Identify what deserves commentary - I examine code and recognize which decisions are non-obvious, marking places where someone reading the implementation alone would reasonably ask "why this way?" This includes edge case handling that looks arbitrary, constraints that aren't expressed in types, workarounds for external behavior, and choices that appear suboptimal but serve important purposes, because these are where future understanding breaks down without preserved context.
Gather knowable information - I collect evidence about the "why" from sources that exist in or near the code, including function and variable names that signal intent, git blame to find when and why changes were made, test names and cases that reveal requirements, error messages that document failure modes, and patterns in neighboring code that establish conventions, understanding that each of these artifacts contains partial information about why the code took its current shape.
Distinguish commentary from description - I ensure comments explain why rather than what, recognizing that "loops through items" repeats what the code already says while "must iterate in insertion order to preserve user's arrangement" explains a constraint the implementation serves, because readers can see the loop but cannot see the requirement that made iteration order matter.
Place comments at decision points - I position commentary where understanding diverges from expectation, putting explanations immediately before the code they explain so readers encounter the "why" just as they're forming questions about the "what," and I avoid cluttering straightforward code with obvious statements because dense commentary trains readers to ignore all comments equally.
Write in natural sentence case - I compose comments using standard capitalization and punctuation, letting the placement and content communicate importance rather than relying on formatting conventions like all-caps words or prefixes such as "WARNING:" that lose meaning through overuse, trusting that a comment positioned at the right decision point and explaining actual constraints conveys its significance through substance rather than typographic emphasis.
Update comments as code evolves - When I modify code, I examine surrounding comments to verify they still reflect current reality, recognizing that outdated comments are worse than no comments because they actively mislead, and I remove comments that no longer serve a purpose rather than accumulating archaeological layers of historical but irrelevant commentary.
Document constraints and gotchas first - I prioritize commenting code where the "simple" or "obvious" approach doesn't work, explaining what was tried and why it failed, because these are the places where future developers will be tempted to "fix" the code by reintroducing problems that were already solved, and preserving that knowledge prevents repeated discovery of the same failure modes.
I'm reviewing a search ranking algorithm that applies several transformations to user queries before matching. The code has a sequence of string operations that look arbitrary - lowercasing, then removing certain punctuation but preserving hyphens, then stemming, then expanding synonyms in a specific order. Reading the implementation, I can see what it does but not why each step happens in this exact sequence. I check git blame and find that synonym expansion was moved after stemming six months ago with message "prevent synonym explosion." I look at the test suite and find cases like "e-mail" that verify hyphen preservation, with test names mentioning "maintain compound word boundaries." I examine error logs and find that the original order caused queries like "running shoes" to expand to dozens of variations before stemming, creating performance issues. Now I have knowable facts about why. I add a comment before the transformation pipeline: "Query normalization order matters. Stemming before synonym expansion prevents exponential variant growth (see issue-847). Hyphen preservation maintains compound terms like 'e-mail' which have different meanings than 'email' in our product catalog." This comment doesn't repeat what the code does - readers can see the method calls - but it explains the constraints that make this particular sequence necessary. Future developers now understand that reordering these steps isn't a simple refactoring but would reintroduce solved problems.
I'm examining error handling code in an HTTP client that retries failed requests, but only for certain status codes. The implementation retries 503 and 429 but not 500, which seems inconsistent since all three are server errors. I check git history and find this was changed from "retry all 5xx" three months ago with message "stop retrying 500 from payments API." I look for related changes and find a comment in the payments integration explaining that their API returns 500 for business logic failures like "insufficient funds" that will never succeed on retry. The test suite confirms this with cases verifying that 500 responses are surfaced immediately rather than retried. Now I understand this isn't arbitrary but a workaround for how a specific integration misuses HTTP status codes. I add a comment above the retry logic: "We retry 503 and 429 but not 500 because the payments provider returns 500 for permanent business logic failures that should not be retried. This violates HTTP semantics but matches their documented behavior. See: https://docs.payments.example/errors." I include the URL because that's knowable information that helps future readers verify the constraint still exists. If someone later suggests "fixing" the retry logic to handle all 5xx codes consistently, they'll see this comment, understand why it was deliberately limited, and can check whether the payments API behavior has changed before making that decision.
I'm reading database query code that builds SQL with manual string concatenation instead of using the query builder that's used everywhere else in the codebase. This looks like technical debt - inconsistent with surrounding patterns, harder to read than the builder API, and potentially vulnerable to injection if misused. My instinct is to refactor it to use the builder. Before doing so, I check git history and find this was actually changed from the query builder two years ago with message "bypass builder for bulk operations." I examine the commit diff and see the original version using the builder was timing out on large datasets. The test suite has performance tests verifying this query completes within specific time bounds for datasets of 10,000+ rows. I check the query builder's source and confirm it adds overhead by generating intermediate objects that aren't needed for this simple batch insert pattern. Now I understand this apparent inconsistency is actually a necessary optimization. I add a comment: "Direct SQL construction instead of query builder to avoid allocation overhead during bulk inserts. The builder pattern creates intermediate objects that cause timeouts above 10k rows. Verified safe against injection because inputs are generated internally from validated IDs, never from user input." This explains both why we're not using the standard pattern and why the usual concerns about manual SQL construction don't apply here. Future developers who encounter this inconsistency will understand it's intentional and the conditions under which it could be safely refactored - if the query builder's performance improves or if these inserts are moved to a background job where latency matters less.