Code Review That Improves Code and Grows Engineers
Battle-tested review practices beyond catching bugs: calibrating feedback, sizing reviews, and a culture that lifts both code quality and the team.

Code Review Is a Design Conversation
Most teams treat code review as a gatekeeping exercise—does the code compile, pass tests, and follow style rules? Linters handle that. The real value of code review is as a design conversation between engineers who see the codebase from different angles.
The reviewer's job is not to prove they found bugs. It is to ensure the code communicates its intent clearly, handles edge cases the author might not have considered, and fits coherently within the broader system.
Calibrating Feedback Severity
The single biggest improvement to any review process is labeling feedback by severity. Without labels, every comment feels like a blocker. With them, the author knows exactly what requires changes and what is optional.
// ❌ Unlabeled review comments — ambiguous severity
// "This could be a map instead of a forEach"
// "Missing null check here"
// "Consider extracting this into a helper"
// ✅ Labeled review comments — clear expectations
// [blocking] Missing null check on user input — this will throw in production
// [suggestion] A map() would be more idiomatic here, but either works
// [nit] Variable name `d` could be more descriptive — maybe `document`?
// [question] What happens if the queue is empty? I don't see that case handled
type ReviewSeverity = "blocking" | "suggestion" | "nit" | "question" | "praise";
interface ReviewComment {
severity: ReviewSeverity;
line: number;
file: string;
comment: string;
suggestedCode?: string;
}
function shouldBlockMerge(comments: ReviewComment[]): boolean {
return comments.some((c) => c.severity === "blocking");
}
function categorizeReview(comments: ReviewComment[]): {
mustFix: ReviewComment[];
shouldConsider: ReviewComment[];
optional: ReviewComment[];
} {
return {
mustFix: comments.filter((c) => c.severity === "blocking"),
shouldConsider: comments.filter(
(c) => c.severity === "suggestion" || c.severity === "question"
),
optional: comments.filter(
(c) => c.severity === "nit" || c.severity === "praise"
),
};
}Sizing Pull Requests for Reviewability
Large PRs get rubber-stamped. Small, focused PRs get genuine review. The data consistently shows that review quality drops sharply after 400 lines of changes. Structure your work to stay under that threshold.
interface PullRequestMetrics {
filesChanged: number;
linesAdded: number;
linesRemoved: number;
totalDelta: number;
reviewTimeEstimate: string;
}
function assessReviewability(metrics: PullRequestMetrics): {
rating: "excellent" | "good" | "risky" | "too-large";
recommendation: string;
} {
const { totalDelta, filesChanged } = metrics;
if (totalDelta <= 200 && filesChanged <= 5) {
return {
rating: "excellent",
recommendation: "Quick review — focused and easy to reason about",
};
}
if (totalDelta <= 400 && filesChanged <= 10) {
return {
rating: "good",
recommendation: "Standard review — allow 30-60 minutes",
};
}
if (totalDelta <= 800) {
return {
rating: "risky",
recommendation:
"Large PR — consider splitting. Review quality will degrade past 400 lines.",
};
}
return {
rating: "too-large",
recommendation:
"Split this PR. Reviewers will skim rather than analyze at this size.",
};
}What to Look For Beyond Style
Automated tools catch formatting, unused imports, and type errors. Human review should focus on what machines cannot evaluate: design coherence, edge cases, naming clarity, and hidden assumptions.
interface ReviewChecklist {
category: string;
questions: string[];
}
const humanReviewChecklist: ReviewChecklist[] = [
{
category: "Design",
questions: [
"Does this change belong in this module, or is it a sign of misplaced responsibility?",
"Will this approach still work when requirements change in the obvious ways?",
"Are there simpler alternatives the author might not have considered?",
],
},
{
category: "Edge Cases",
questions: [
"What happens with empty input? Null? Undefined?",
"What if this is called concurrently?",
"What if the external service is down or slow?",
],
},
{
category: "Naming and Intent",
questions: [
"Can I understand what this function does from its name alone?",
"Do variable names communicate their purpose without reading usage?",
"Would a new team member understand this code in six months?",
],
},
{
category: "Hidden Assumptions",
questions: [
"What implicit ordering or state does this code depend on?",
"Are there environment-specific assumptions baked in?",
"Does this silently degrade or loudly fail on bad input?",
],
},
];Giving Feedback That Teaches
The best review comments do not just point out problems—they explain the underlying principle so the author avoids the same pattern next time. The goal is to make the next PR better, not just this one.
// ❌ Feedback that corrects without teaching
// "Use Promise.all here instead of sequential awaits"
// ✅ Feedback that explains the principle
// [suggestion] These three API calls are independent — they don't depend
// on each other's results. Running them sequentially adds ~600ms of
// unnecessary latency. Promise.all lets them execute concurrently:
//
// const [users, orders, inventory] = await Promise.all([
// fetchUsers(),
// fetchOrders(),
// fetchInventory(),
// ]);
//
// General principle: sequential awaits are correct when each call depends
// on the previous result. For independent calls, always parallelize.
interface TeachingComment extends ReviewComment {
principle: string;
example?: string;
resources?: string[];
}
const exampleFeedback: TeachingComment = {
severity: "suggestion",
line: 42,
file: "src/services/dashboard.ts",
comment:
"These API calls can run in parallel since they're independent.",
principle:
"Use sequential await when calls depend on previous results. " +
"Use Promise.all when calls are independent.",
suggestedCode: `const [users, orders] = await Promise.all([
fetchUsers(teamId),
fetchOrders(teamId),
]);`,
};Building a Review Culture
Individual review practices matter less than team norms. A healthy review culture has explicit agreements about response times, comment labels, and what qualifies as blocking.
interface ReviewAgreement {
maxResponseTimeHours: number;
maxPRSizeLines: number;
requiredApprovals: number;
commentLabels: ReviewSeverity[];
selfReviewBeforeSubmit: boolean;
prDescriptionTemplate: string[];
}
const teamAgreement: ReviewAgreement = {
maxResponseTimeHours: 4,
maxPRSizeLines: 400,
requiredApprovals: 1,
commentLabels: ["blocking", "suggestion", "nit", "question", "praise"],
selfReviewBeforeSubmit: true,
prDescriptionTemplate: [
"## What",
"Brief description of the change",
"## Why",
"Context and motivation",
"## How to test",
"Steps for the reviewer to verify",
"## Risks",
"What could go wrong and how it's mitigated",
],
};Key Takeaways
Label every review comment by severity so authors know what is blocking versus optional. Keep pull requests under 400 lines—review quality drops sharply beyond that, and large PRs get rubber-stamped rather than reviewed. Focus human review on design coherence, edge cases, naming clarity, and hidden assumptions; leave style enforcement to linters.
Write feedback that teaches the underlying principle, not just the fix. A comment that explains why parallel execution matters helps the engineer write better code in every future PR, not just this one. Establish team-level review agreements covering response times, PR sizing, and comment conventions—culture scales better than individual habit.


