Code Review Best Practices: Giving and Receiving Feedback That Matters
Master code reviews that improve code quality and developer growth. Templates, frameworks, and communication strategies for effective feedback.
Code Review Best Practices: Giving and Receiving Feedback That Matters
Code reviews are where junior developers become senior developers. They're where knowledge transfers, bugs are discovered before they reach production, and standards are established. Yet many teams treat code reviews as a bureaucratic checkbox rather than a learning opportunity.
Good code reviews require skill—the ability to give honest feedback without being harsh, to recognize patterns others miss, and to explain why something matters. It's not about being "right"; it's about making the whole team better.
Why Code Reviews Matter
Research shows teams with strong code review practices:
- 50% fewer bugs make it to production
- 3x faster onboarding for new developers
- Higher code consistency across the codebase
- Better knowledge distribution (not just in senior devs' heads)
- Fewer security vulnerabilities caught before release
Reviewer Responsibilities
1. Check for Correctness
// ❌ REVIEW CONCERN: Logic issue
function validateEmail(email: string): boolean {
// Does @ exist? Yes.
// Is it a valid email? No.
return email.includes('@');
}
// ✅ REVIEWER FEEDBACK: "This validation is too loose.
// Valid emails require format: local@domain.extension.
// Consider using a regex or validator library."
// ✅ BETTER: Using zod
import { z } from 'zod';
const emailSchema = z.string().email();
function validateEmail(email: string): boolean {
try {
emailSchema.parse(email);
return true;
} catch {
return false;
}
}
2. Identify Performance Issues
// ❌ REVIEW CONCERN: N+1 query problem
async function getUsersWithPosts(userIds: string[]) {
const users = await db.users.find({ id: { in: userIds } });
// This runs a query for EACH user — if 100 users, 100 queries!
return users.map(async (user) => {
const posts = await db.posts.find({ userId: user.id });
return { ...user, posts };
});
}
// ✅ REVIEWER FEEDBACK: "This causes N+1 queries.
// For 100 users, you're running 101 database queries.
// Use a JOIN or batch query instead."
// ✅ BETTER: Single query with join
async function getUsersWithPosts(userIds: string[]) {
return db.users
.find({ id: { in: userIds } })
.join('posts', 'users.id = posts.user_id');
}
3. Enforce Code Consistency
// ❌ REVIEW CONCERN: Inconsistent error handling
// In file A:
async function fetchUser() {
try {
const response = await fetch('/api/user');
return response.json();
} catch (error) {
console.log('Error:', error); // Logs to console
}
}
// In file B:
async function fetchPosts() {
try {
const response = await fetch('/api/posts');
return response.json();
} catch (error) {
return null; // Silently fails
}
}
// ✅ REVIEWER FEEDBACK: "Error handling differs between
// these similar functions. Let's establish a standard
// approach—should we log, throw, or return null?"
// ✅ BETTER: Create shared error handler
export async function apiRequest<T>(
url: string,
options?: RequestInit
): Promise<T> {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new ApiError(`HTTP ${response.status}`, response.status);
}
return response.json();
} catch (error) {
logger.error('API request failed', { url, error });
throw error;
}
}
4. Suggest Better Patterns
// ❌ REVIEW CONCERN: Prop drilling
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <div>{user.name}</div>;
}
// ✅ REVIEWER FEEDBACK: "Passing user through 3 components
// makes it hard to refactor. Consider using Context API or
// (better) Zustand for state management."
// ✅ BETTER: Use Context
const UserContext = createContext<User | null>(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
function UserMenu() {
const user = useContext(UserContext);
return <div>{user?.name}</div>;
}
Reviewer Code Review Template
## Code Review Checklist
### ✅ Functionality
- [ ] Code works as intended
- [ ] Edge cases are handled
- [ ] No console.log or debugger statements
- [ ] Error handling is robust
### ✅ Performance
- [ ] No N+1 queries
- [ ] Unnecessary re-renders are avoided
- [ ] Bundle size impact is acceptable
- [ ] No memory leaks
### ✅ Code Quality
- [ ] Follows project conventions
- [ ] Type-safe (no `any` types)
- [ ] Functions are testable
- [ ] Code is readable without comments
### ✅ Security
- [ ] No hardcoded secrets
- [ ] User input is validated
- [ ] No SQL injection vulnerabilities
- [ ] CSRF protection is in place
### ✅ Tests
- [ ] New code has tests
- [ ] Existing tests still pass
- [ ] Test coverage didn't decrease
### Questions (Not Blockers)
- [ ] Have you considered [alternative approach]?
- [ ] What happens if [edge case] occurs?
---
**Overall Assessment:**
- [ ] Approved ✅
- [ ] Approve with minor comments 👌
- [ ] Request changes 🔧
How to Give Effective Feedback
1. Start Positive
❌ "This error handling is bad."
✅ "Good thinking to add error handling here.
One small suggestion — let's be more specific..."
2. Explain the "Why"
❌ "Use const instead of let."
✅ "Use const instead of let. This signals intent to
readers that this variable won't be reassigned,
which makes reasoning about code easier."
3. Offer Solutions
❌ "This query is slow."
✅ "This query is O(n²) because you're iterating
data twice. You could use .reduce() to do it
in one pass:
[example code]"
4. Distinguish Requirements from Suggestions
❌ (Everything sounds like a requirement)
✅
**Must Fix:**
- Remove console.log on line 42
- Add ErrorBoundary (currently missing error handling)
**Consider:**
- Could extract this logic to a helper function
- FYI: Using Zod makes validation cleaner
Receiving Code Review Feedback
Don't Take It Personally
Feedback on code ≠ Feedback on you
❌ "They're saying my code is bad"
✅ "They found potential improvements"
Ask Questions
If feedback is unclear:
💬 "I'm not sure I understand — can you elaborate
on why this pattern is problematic?"
Explain Your Reasoning
If you have a good reason:
💬 "I initially considered that approach, but chose
this because [constraint/reason]. Thoughts?"
Know When to Disagree
Not all feedback is correct. It's OK to push back respectfully:
💬 "I appreciate the suggestion, but I think keeping
it this way is better because [reason].
What's your take?"
Code Review Automation
Not everything needs human review. Automate what you can:
# .github/workflows/pr-checks.yml
name: Automated Checks
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run lint # ESLint, Prettier
- run: npm run type-check # TypeScript
- run: npm run test # Unit tests
- run: npm run test:e2e # E2E tests
Tools handle:
- ✅ Code style (ESLint, Prettier)
- ✅ Type safety (TypeScript)
- ✅ Tests pass
- ✅ Bundle size impact
- ✅ Security scanning (Snyk, OWASP)
Humans focus on:
- ✅ Architecture decisions
- ✅ Business logic
- ✅ Performance implications
- ✅ Knowledge transfer
Time Constraints
Code reviews should be fast. Target:
- < 100 lines: 15 minutes
- 100-300 lines: 30 minutes
- > 300 lines: Request refactoring into smaller PRs
Large PRs are hard to review thoroughly and slow down shipping. Create a culture where "small, focused PRs" is the norm.
Building a Review Culture
- Psychological Safety: Reviews shouldn't feel like attacks
- Growth Mindset: Framing as learning, not judgment
- Consistency: Same standards for everyone
- Speed: Respond to reviews quickly
- Respect: Trust that reviewers have good intent
Conclusion
Code reviews are one of the most powerful tools for improving code quality and team cohesion. The difference between perfunctory reviews ("looks good") and thoughtful reviews is the difference between stagnant and continuously improving engineering teams.
Invest in review culture. It pays dividends in code quality, knowledge sharing, and team morale.