Skip to content

Effective Code Reviews: Beyond Nitpicking and Rubber Stamps

How to give code reviews that catch real bugs, share knowledge, and improve team velocity instead of creating bottlenecks.

4 min read
Pull request interface showing constructive code review comments

Code review is the most powerful quality tool that teams consistently misuse. At one extreme, reviews devolve into style nitpicking — arguing about bracket placement while a race condition ships to production. At the other extreme, reviewers rubber-stamp PRs with "LGTM" after a 30-second glance, turning review into an empty ritual.

The goal of code review is not perfection. It is catching defects that automated tools cannot, sharing knowledge across the team, and maintaining a codebase that the whole team can work in confidently.

What to Look For

A structured review checklist prevents the two failure modes: spending 20 minutes on formatting while missing a security hole, and approving without actually reading the code.

tstypescript
interface ReviewChecklist {
  // High priority — these create production incidents
  correctness: [
    "Does the code do what the PR description says?",
    "Are edge cases handled (null, empty, max values)?",
    "Are error paths handled, not just the happy path?",
  ];
  security: [
    "Is user input validated and sanitized?",
    "Are authorization checks in place?",
    "Is sensitive data handled properly (no logging PII)?",
  ];
  // Medium priority — these create long-term pain
  design: [
    "Is this the right abstraction level?",
    "Will this be maintainable by someone who didn't write it?",
    "Does it follow existing patterns in the codebase?",
  ];
  // Low priority — automate these away
  style: [
    "Let the linter handle this.",
    "Seriously, configure the linter.",
  ];
}

Review time is finite. Spend it on correctness and security first, design second, and never on style — that is what prettier and ESLint exist for.

Writing Useful Review Comments

The difference between a helpful review and a demoralizing one is framing. Comments should explain the concern, not just point out what is wrong.

tstypescript
// ❌ Unhelpful — what should they do instead?
// "This is wrong."
// "Don't do it this way."
// "Nit: use const here."
 
// ✅ Helpful — explains the concern and suggests an alternative
// "This query runs inside the loop, which will cause N+1
// queries at scale. Consider using a JOIN or batch query
// to load all related records in one call."
 
// ✅ Questions work better than commands for design decisions
// "What happens if this promise rejects? I don't see error
// handling — is it intentional to let it bubble up?"

Prefix comments with their severity:

markdownmarkdown
**blocker**: This will cause data loss in production. The DELETE
query has no WHERE clause when `userId` is undefined.
 
**suggestion**: Consider extracting this into a utility function —
I've seen this pattern in three other files.
 
**question**: Is the timeout of 30s intentional? Our SLA is 5s
for this endpoint.
 
**nit**: Minor style preference, not blocking. Take it or leave it.

The prefix tells the author what must be fixed versus what is optional. Without it, authors treat every comment as a blocker, creating frustration on both sides.

The Author's Responsibility

Good reviews start with good PRs. A reviewer working with a 2000-line PR and no description is set up to fail.

markdownmarkdown
<!-- ❌ PR description that wastes reviewer time -->
## Changes
Updated the user service.
 
<!-- ✅ PR description that enables quality review -->
## Context
Users reported intermittent 500 errors during checkout.
Root cause: race condition in inventory reservation.
 
## Changes
- Added optimistic locking to inventory updates
- Added retry logic for concurrent modification errors
- Added integration test reproducing the race condition
 
## Testing
- [x] Reproduced the race condition with parallel requests
- [x] Verified fix under concurrent load (k6 script attached)
- [x] Existing tests pass
 
## Risks
- Retry logic adds ~50ms latency in the contention case
- Optimistic locking may surface errors in other flows
  that were silently succeeding with stale data

PR Size Matters

tstypescript
const reviewEffectiveness = {
  "1-100 lines": { defectRate: "high", reviewTime: "15 min" },
  "100-400 lines": { defectRate: "medium", reviewTime: "30-60 min" },
  "400-1000 lines": { defectRate: "low", reviewTime: "60+ min" },
  "1000+ lines": { defectRate: "near-zero", reviewTime: "rubber stamp" },
};
// Research shows defect detection drops dramatically above 400 lines

If your PR is over 400 lines, split it. Stack PRs on feature branches if the changes are sequential. Reviewers have a finite attention budget — large PRs exhaust it before they reach the critical code.

Review Anti-Patterns

The Gatekeeper

tstypescript
// ❌ Gatekeeper review — imposes personal preferences as requirements
// "I would have done this differently. Please rewrite using
// the visitor pattern instead of the switch statement."
 
// ✅ Collaborative review — explains trade-offs
// "A switch statement works here. If we expect more than 5-6
// cases, a strategy pattern might be easier to extend. For now,
// this is fine — just flagging for future reference."

The gatekeeper treats every review as an opportunity to rewrite the code their way. This creates a bottleneck, demoralizes authors, and does not improve quality.

The Perfectionist

tstypescript
// ❌ Blocking on subjective preferences
// "Please rename `processData` to `transformUserRecords`."
// (4 rounds of review later, still debating the name)
 
// ✅ Approve and suggest
// "Approving — the logic is correct and well-tested.
// Optional: `transformUserRecords` might be more descriptive
// than `processData`, but not blocking on this."

Approve the PR when it is correct and safe, even if you would have written it differently. Preferences belong in style guides and linters, not in code review.

Automating the Boring Parts

Every manual review comment about formatting, import order, or naming conventions is a process failure. Automate style enforcement so humans can focus on logic.

jsonjson
{
  "scripts": {
    "lint": "eslint . --max-warnings 0",
    "format:check": "prettier --check .",
    "typecheck": "tsc --noEmit"
  }
}
ymlyaml
# .github/workflows/pr-checks.yml
name: PR Checks
on: [pull_request]
jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm run lint
      - run: npm run format:check
      - run: npm run typecheck
      - run: npm test

If these checks pass before a human sees the PR, the reviewer never needs to comment about semicolons, unused imports, or type errors. Their review is purely about logic, design, and correctness.

Building a Review Culture

The process only works if the team values it. Two practices build a healthy review culture:

Timely reviews: Set a team norm — PRs should receive first review within 4 hours during work hours. Stale PRs cause merge conflicts, context switching, and frustration. If you are blocking someone, their PR is your priority.

Review as learning: Junior engineers reviewing senior code is just as valuable as the reverse. The junior learns patterns and context. The senior gets a fresh perspective and practices explaining decisions. Make it bidirectional.

The best review culture is one where nobody dreads opening a PR or receiving feedback. That starts with treating reviews as collaborative problem-solving, not code audits.

Key Takeaways

  1. Prioritize correctness and security over style — let linters handle formatting so you can focus on bugs
  2. Prefix comments with severity — blocker, suggestion, question, nit — so authors know what must be fixed
  3. Write descriptive PR descriptions — context, changes, testing done, and risks enable better reviews
  4. Keep PRs under 400 lines — defect detection drops dramatically for larger changes
  5. Approve when correct, suggest when optional — blocking on preferences creates bottlenecks without improving quality
  6. Review within 4 hours — stale PRs compound into merge conflicts and lost context
Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX