How to Review Code Without Making Enemies
How to Review Code Without Making Enemies
Code review is one of the most effective practices for improving software quality, sharing knowledge, and catching defects early. Yet it remains one of the most socially fraught activities in software engineering. A well-intentioned comment can be perceived as a personal attack. A straightforward suggestion can trigger defensiveness. Over time, poorly handled reviews erode team trust, slow delivery, and increase turnover.
The goal of this post is not to tell you how to find bugs or enforce style guides. Rather, it is to provide a practical framework for conducting code reviews that preserve relationships while maintaining high standards. The techniques described here are drawn from years of experience on teams ranging from two-person startups to large-scale distributed engineering organizations.
Separate the Person from the Code
The most common mistake in code review is conflating the author's identity with the code they wrote. When you write "this is wrong," the author may hear "you are wrong." This is not a matter of thin skin; it is a cognitive bias known as fundamental attribution error, where we attribute others' mistakes to their character rather than to circumstances.
To mitigate this, adopt a linguistic pattern that focuses on the code itself. Instead of "You forgot to handle the null case," try "The null case does not appear to be handled here." Instead of "This logic is confusing," try "The logic in this block is difficult for me to follow."
Consider this concrete example. A developer submits a pull request with a function that computes a discount:
def apply_discount(price, discount):
if discount > 1:
return price * (1 - discount / 100)
else:
return price * (1 - discount)
A poor review comment might be: "You have a bug here. The discount logic is inconsistent." A better comment: "It looks like this function treats discounts above 1 as percentages and below 1 as decimals. Is that intentional? Standardizing on one format could prevent confusion."
The second version acknowledges the author's intent, asks a clarifying question, and proposes a path forward. It does not assign blame. It invites collaboration.
Ask Questions Before Making Statements
When you encounter code that seems suboptimal, your first instinct may be to prescribe a fix. Resist this urge. Instead, ask a question that reveals the author's reasoning. This serves two purposes: it gives the author the benefit of the doubt (they may have a valid constraint you do not see), and it frames the review as a dialogue rather than a verdict.
For example, you see a method with a long parameter list:
public void createUser(String name, String email, String phone, String address, String city, String state, String zip)
Instead of writing "Refactor this to use a User object," try "Is there a reason not to encapsulate these parameters into a User object? I find long parameter lists harder to test and maintain."
This approach works because it acknowledges that the author may have made a deliberate trade-off. Perhaps they were following an existing pattern in the codebase. Perhaps they were under time pressure. By asking, you learn something, and the author feels respected.
Data from a 2019 study of open-source pull requests on GitHub found that reviews with more questions and fewer commands resulted in faster merge times and higher author satisfaction. The difference was statistically significant. Questions reduce friction.
Provide Context for Every Suggestion
A common frustration for code authors is receiving feedback without justification. Comments like "Use const here" or "This should be a switch statement" are unhelpful because they do not explain why. The author may comply, but they learn nothing, and they may resent being told what to do without explanation.
Every suggestion should include a rationale. This rationale can reference a team convention, a performance concern, a readability argument, or a broader design principle. For instance:
- "Using
consthere prevents accidental reassignment and makes the variable's intent clearer to future readers." - "A switch statement would make the branching logic more explicit, which helps when we add new cases later."
Consider this JavaScript example:
if (status === 'active' || status === 'pending') {
// do something
}
Instead of "Use an array," write: "If we extract the valid statuses into a Set, like const ACTIVE_STATUSES = new Set(['active', 'pending']), then the condition becomes ACTIVE_STATUSES.has(status). This scales better as more statuses are added and centralizes the definition."
The second version teaches a technique. It explains the "why" behind the "what." Over time, this builds a team culture where feedback is seen as learning, not criticism.
Be Explicit About Severity and Scope
Not all feedback is equal. A typo in a comment is not the same as a race condition in production code. Yet many reviewers treat all comments with the same tone and urgency. This creates noise and confusion.
Adopt a labeling system for your review comments. Some teams use prefixes like nit:, blocker:, suggestion:, or question:. Others use emoji or tags. The key is to signal the expected action.
- Blocker: The code cannot be merged as-is. This should be reserved for correctness issues, security vulnerabilities, or violations of core architecture.
- Suggestion: An improvement that would be nice but is not required. The author may accept or defer.
- Nitpick: Minor style or formatting issues. These are often better left to automated formatters.
For example, in a code review comment:
blocker: The password is being stored in plaintext. Use bcrypt or Argon2 for hashing before persisting.
nit: Extra blank line at end of file.
By being explicit, you reduce anxiety. The author knows which comments demand immediate attention and which are optional. This also prevents the reviewer from appearing overly critical when most of their feedback is cosmetic.
Offer Alternatives, Not Ultimatums
The most toxic code review comments are those that present a single solution as the only correct one. This shuts down discussion and disrespects the author's agency. Instead, offer multiple alternatives, or frame your suggestion as one of several valid approaches.
For instance, you see a method that is 80 lines long. Instead of "Break this method up," try "This method is doing several things. One approach is to extract the validation logic into a separate method. Another is to split it into a pipeline of smaller functions. What do you think would be clearest here?"
This approach has two benefits. First, it acknowledges that there is often more than one right answer. Second, it gives the author ownership of the final decision. They may choose one of your suggestions or propose a third option. Either way, they leave the review feeling empowered rather than controlled.
In practice, I have found that when I offer alternatives, the author often implements a hybrid solution that is better than what either of us originally proposed. The review becomes a design discussion rather than a compliance audit.
Conclusion
Code review is a social practice as much as a technical one. The best reviewers are not those who catch the most bugs, but those who help their teammates grow while maintaining high standards. By separating the person from the code, asking questions before making statements, providing context for suggestions, being explicit about severity, and offering alternatives instead of ultimatums, you can review code without making enemies.
The result is a team that ships better software faster, because trust replaces tension, and collaboration replaces confrontation.