How to Do Code Reviews That Actually Help
Code reviews can be the most valuable part of your development process or a frustrating waste of time. The difference isn't the code—it's how you approach the review.
Here's how to make code reviews actually useful.
Why Code Reviews Matter
Code reviews catch bugs before they reach production. They spread knowledge across the team. They maintain code quality and consistency. They're one of the few times you get direct feedback on your work.
But done poorly, they create friction, slow down delivery, and breed resentment. I've seen teams where developers dread reviews because they turn into nitpicking sessions or personal attacks.
Aha moment: Early in my career, I left a comment saying "This is wrong." The developer got defensive, I got frustrated, and we wasted an hour arguing. I should have said "Have you considered how this handles null values?" Same concern, completely different tone.
Before You Start Reviewing
Set the right context. Read the PR description. Understand what problem is being solved. Check related issues or tickets. Don't just jump into the code blind.
Pull the branch and run it locally if it's a significant change. Screenshots and descriptions are great, but nothing beats seeing it work yourself.
Quick checklist before reviewing:
□ Read the PR description and linked issues
□ Understand the context and goals
□ Check if tests are included and passing
□ Note the size—if it's 1000+ lines, ask for it to be split
What to Look For
Reviews have different levels. Don't get stuck on formatting when there are architectural issues.
Level 1: Does it work?
Does the code solve the problem? Are there obvious bugs? Does it handle edge cases? This is your first pass—catch the critical issues.
// ❌ Bug: Division by zero
function calculateAverage(total: number, count: number) {
return total / count;
}
// ✓ Better: Handle zero case
function calculateAverage(total: number, count: number) {
if (count === 0) return 0;
return total / count;
}
Level 2: Is it maintainable?
Can other developers understand this code? Are variable names clear? Is the logic straightforward? Will this be easy to debug in six months?
// ❌ Hard to understand
function processData(d: any[]) {
return d.filter(x => x.s === 'a').map(x => x.v * 1.2);
}
// ✓ Clear intent
function getActivePricesWithTax(products: Product[]) {
const activeProducts = products.filter(p => p.status === 'active');
const TAX_RATE = 1.2;
return activeProducts.map(p => p.price * TAX_RATE);
}
Level 3: Is it well-designed?
Is this the right approach? Are there better patterns? Does it fit the existing architecture? Is it testable?
Level 4: Performance and security
Are there N+1 queries? Memory leaks? Security vulnerabilities? Does it handle user input safely?
// ❌ Security issue: SQL injection risk
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ✓ Better: Use parameterized queries
const query = 'SELECT * FROM users WHERE email = ?';
db.execute(query, [email]);
Level 5: Tests
Are there tests? Do they cover the important cases? Are they testing behavior, not implementation?
How to Write Good Review Comments
Your tone matters more than you think. Code reviews happen asynchronously, so there's no body language or voice to soften your words. Text comes across harsher than you intend.
❌ Bad comments:
"This is wrong."
"Why did you do it this way?"
"This doesn't make sense."
"You should know better."
✓ Good comments:
"This might not handle the case where the array is empty. Have you tested that?"
"I'm curious about this approach. Could you explain the reasoning?"
"I'm having trouble following this logic. Could you add a comment explaining it?"
"Consider using Array.find() here—it's more readable and stops at the first match."
Use questions, not commands:
"Could we extract this into a separate function?" is better than "Extract this into a separate function." The first invites discussion. The second feels like an order.
Explain why:
Don't just point out issues—explain why they matter. "This could cause a memory leak because the event listener is never removed" is more helpful than "Remove the event listener."
Offer alternatives:
// Instead of: "This is inefficient"
// Say: "This loops through the array multiple times.
// Consider combining the filter and map into a single pass:
const result = products.reduce((acc, product) => {
if (product.status === 'active') {
acc.push(product.price * TAX_RATE);
}
return acc;
}, []);"
Distinguish between must-fix and suggestions:
Use prefixes to clarify: "Nit: Consider renaming this variable" vs "Critical: This doesn't handle the null case."
My system: I use "Nit" for minor style things, "Question" when I'm curious, "Suggestion" for improvements, and "Issue" for actual bugs. This helps authors prioritize.
The Praise Sandwich Is Real
Don't just point out problems. Call out good code too. "Great error handling here!" or "This is really clean" takes five seconds and makes a huge difference to morale.
If someone solved a hard problem elegantly, say so. If they wrote clear documentation, acknowledge it. Code reviews shouldn't be just about finding faults.
Aha moment: I once reviewed a junior developer's PR and only left critical comments. They implemented everything correctly, but I made them feel like they did a terrible job. Now I always start with what they did well.
Common Mistakes Reviewers Make
Bikeshedding: Spending 20 minutes debating variable names while missing a critical bug. Focus on what matters.
Nitpicking style: If you have formatters and linters, style should be automated. Don't waste review time on tabs vs spaces.
Rewriting in your style: Just because you'd write it differently doesn't mean their approach is wrong. Unless there's a functional issue, let their code style through.
Reviewing when you're tired or frustrated: Your comments will be harsher than you intend. Take a break and come back.
Not explaining yourself: "This is bad" helps no one. Explain why it's bad and what would be better.
Blocking on personal preference: Unless it violates team standards or creates bugs, approve it. You're not the code police.
Receiving Reviews: The Other Side
Getting feedback on your code is hard. It feels personal even when it's not. Here's how to handle it better.
Don't take it personally:
Reviews are about the code, not you. The reviewer isn't saying you're a bad developer—they're trying to make the code better.
Ask questions if unclear:
If a comment doesn't make sense, ask for clarification. "Can you explain what you mean by this?" is always valid.
Push back when appropriate:
If you disagree with feedback, explain your reasoning. Maybe you considered their suggestion and chose this approach for a reason. That's fine—just communicate it.
// Good pushback example:
"I considered using a Map here, but the dataset is small (<10 items)
and we're only doing a single lookup. An array with find() is simpler
and the performance difference is negligible for our use case."
Don't argue in comments:
If you're going back and forth more than twice, hop on a call. Text-based arguments escalate quickly.
Fix the easy stuff immediately:
If someone points out a typo or suggests a better variable name and you agree, just fix it. Don't debate minor things.
Aha moment: I once spent an hour defending my approach in review comments. When we finally talked face-to-face, we resolved it in five minutes. Now if there's any friction, I suggest a quick call immediately.
PR Best Practices
Keep PRs small:
A 200-line PR gets a thorough review. A 2000-line PR gets a rubber stamp. Break large features into smaller, reviewable chunks.
Write clear descriptions:
// ❌ Bad PR description:
"Fixed the bug"
// ✓ Good PR description:
"Fix race condition in product search
**Problem:** Users could see stale results if they typed
quickly, because older requests completed after newer ones.
**Solution:** Use AbortController to cancel in-flight
requests when a new search starts.
**Testing:** Added test covering rapid search changes."
Add context with comments:
If you made a non-obvious decision, add a comment in the code explaining why. Save the reviewer from having to ask.
Self-review first:
Before requesting review, go through your own diff. Catch typos, remove debug code, and add comments where needed. It shows respect for reviewers' time.
Respond to all comments:
Even if it's just "Done" or "Fixed." It shows you read the feedback and helps reviewers track what's been addressed.
When to Approve vs Request Changes
This is where judgment comes in. Not every comment needs to block merging.
Request changes for:
Bugs or security issues. Broken tests. Missing critical functionality. Architectural problems that will be hard to fix later.
Approve with comments for:
Minor improvements. Style suggestions. "Would be nice to have" additions. Questions that are curiosity, not blockers.
Approve immediately for:
Urgent fixes. Tiny changes (typos, docs). When you trust the author to address minor feedback.
Rule of thumb: If the PR makes things better, approve it. Don't block good progress waiting for perfect. You can always refactor later.
Handling Disagreements
Sometimes you'll disagree with the author. That's normal. Here's how to handle it:
1. Understand their reasoning first. Maybe they know something you don't.
2. Explain your concerns clearly. Not "this is wrong" but "I'm worried this could cause X because Y."
3. Provide evidence if possible. Link to docs, show a test case that fails, or demonstrate the issue.
4. Be willing to be wrong. Maybe their approach is actually better.
5. Escalate if needed. If you can't agree and it matters, involve a senior dev or architect.
6. Move on. Not every hill is worth dying on. If it's not a critical issue, let it go.
Automated Tools Are Your Friend
Don't waste review time on things machines can check. Use linters, formatters, and CI/CD to catch:
Style violations. Failing tests. Type errors. Security vulnerabilities. Missing documentation.
This frees reviewers to focus on logic, architecture, and design—things humans are actually good at.
My CI pipeline always checks:
□ Tests pass
□ Linting passes
□ Type checking passes
□ Code coverage doesn't decrease
□ Build succeeds
Building a Good Review Culture
Code review culture matters more than process. A few principles:
Review promptly: Don't leave PRs sitting for days. Aim to review within a few hours.
Everyone reviews: Junior developers should review too. They learn by seeing others' code and asking questions.
No genius code: If you're the only one who understands it, it's not clever—it's a liability.
Assume good intent: The author isn't trying to write bad code. They're doing their best with the information they have.
Learn from reviews: Both sides should learn. Reviewers see new patterns. Authors get feedback. It's collaborative, not adversarial.
Final thought: The goal isn't perfect code. It's better code than what we had before, written by a team that trusts and learns from each other.
Final Thoughts
Good code reviews make your codebase better and your team stronger. Bad code reviews create resentment and slow everyone down.
The difference is mostly about communication. Be clear, be kind, explain your reasoning, and remember there's a person on the other side who's trying their best.
Review the code, not the person. Ask questions instead of giving orders. Praise good work. Fix the easy stuff. Talk face-to-face when needed.
Code reviews are a skill. You'll get better with practice. Be patient with yourself and others.