Code review is where technical excellence meets team culture. A well-run review process catches bugs, spreads knowledge, and builds trust—but only if reviewers and authors know how to do it right. This practical guide walks you through the patterns we've refined across production teams.
Why code review matters in your workflow
Before we dive into the mechanics, let's be clear about what code review actually does. It's not a gate to keep bad code out—it's a conversation between professionals about intent, trade-offs, and maintainability. When reviews work, they improve code and the team doing the reviewing.
Too many teams treat review as a checkbox. The reviewer skims the diff, leaves a comment about formatting, and approves. The author feels micromanaged. Nothing changes. Code review on GitHub, done well, is completely different. It's a teaching moment for both sides.
The best teams we work with treat a pull request like a documented decision. The description explains why the change exists. The commits are clean and logical. The review conversation is honest and specific. That takes practice, but it scales.
Review the design first — Approach before style
Start your review at the highest level: does this solve the right problem the right way?
Before you look at a single line of code, read the PR description and understand the intent. If there isn't a clear description, send it back and ask for one. This isn't bureaucracy—it's protecting your review time and your codebase. A PR that can't be described in a few sentences probably shouldn't exist as a single PR.
Once you understand the goal, ask yourself:
- Does this approach fit our architecture?
- Are there simpler ways to solve this problem?
- What will this look like to maintain in six months?
- Does it handle the edge cases we know about?
These are design questions. They come before syntax, naming, or test coverage. If the approach is wrong, line-level feedback is noise.
Here's what this looks like in practice. A teammate opens a PR that adds a new database query to the user service:
git log --oneline origin/main..HEAD
a1b2c3d Add getUsersByStatus query to user service
Before reviewing the code, you check the description. It says: "Need to fetch users by their current status for the new dashboard feature."
Your first question isn't about the SQL or the type hints. It's: "Is this a user service responsibility, or should this live in a repository or query builder?" That's a design question. If you flag it now, before the author has written tests or documentation, you save everyone time.
Once you're satisfied the design works, move to implementation details. Not before.
Use suggestions — Suggested changes reviewers can apply in one click
GitHub's suggestion feature is underused. Instead of writing "change const to let here," use the suggestion button to propose the exact change. The author can apply it in one click, or start a conversation about it.
This does two things: it makes your feedback actionable, and it removes ambiguity about what you're asking for.
To leave a suggestion on GitHub, click the ± (suggestion) button when you're composing a review comment. You'll get a side-by-side editor where you show the before and after:
// Before
const userName = getUserData(id);
// Suggestion shows:
const userName = await getUserData(id);
The author sees your suggestion and either applies it directly or discusses why they disagree. If they apply it, GitHub tracks it and shows it in your review history. This creates a paper trail of what changed and why.
Use suggestions for:
- Syntax or naming changes you're confident about
- Test cases that should be added
- Error messages or comments that need tweaking
- Import reorganization or formatting (if automation doesn't catch it)
Don't use suggestions for design decisions or architectural trade-offs. Those need conversation, not one-click approval.
Be kind and specific — Explain the why; offer alternatives
This is where code review moves from mechanics to culture. A comment like "this is inefficient" is demoralizing and useless. A comment like "we're iterating this array twice, which hits performance on large datasets—could we combine the loops?" is concrete and respectful.
Always explain the reasoning behind your feedback. Don't assume the author knew what you knew. Maybe they didn't hit this issue in their local environment. Maybe they prioritized readability over performance and that's the right call. You won't know unless you explain why you're asking for a change.
Better yet, offer an alternative. If you're suggesting a different pattern, show what it looks like:
We usually handle this with a strategy pattern rather than a switch statement—it makes adding new types easier without touching the existing code. Here's what that might look like: [code example]. Happy to discuss if you think the switch is clearer in this case.
Notice the tone: you're not saying "do this." You're saying "here's an option, and here's why we usually prefer it. Let me know if you see it differently." That's the difference between a code reviewer and a code reviewer people respect.
Real example: you notice a function that catches all exceptions silently:
try {
await processPayment(order);
} catch (e) {
// silently fail
}
Bad review comment: "Don't swallow exceptions like this."
Good review comment: "Silent failures on payment processing are hard to debug and can hide real issues. Did we mean to retry here, or should we at least log the error? I've seen [link to similar pattern in codebase] handle this—worth considering."
The difference is that you've explained the risk, shown an example, and acknowledged that there might be a reason the author did it this way. Now they can make an informed decision, and if they disagree, you have a real conversation instead of a mandate.
Automate style — Linters and formatters, not humans, check formatting
Stop spending review time on formatting. Seriously. Linters, formatters, and pre-commit hooks exist to handle this. Use them.
If you're reviewing code and finding yourself leaving comments about indentation, spacing, or whether a variable name should be camelCase or snake_case, you've already lost. The author's productivity goes down. Your review time gets wasted on non-blocking issues. And you build a culture where the coder feels nitpicked instead of mentored.
Configure ESLint, Prettier, Black, or whatever tool your language uses. Run it on every commit. Make it a CI check so that code can't land without passing. Now your review comments are about logic, design, and trade-offs. Not semicolons.
Here's what a practical setup looks like for a Node.js project:
# Install
npm install --save-dev eslint prettier eslint-config-prettier
# Configure .eslintrc.json
{
"extends": ["eslint:recommended", "prettier"]
}
# Add to package.json
"scripts": {
"lint": "eslint .",
"format": "prettier --write ."
}
# Run before commit
npm run lint && npm run format
Now when a PR arrives, formatting is already correct. Your brain is free to focus on the hard problems: is this the right solution? Will it scale? Can the next engineer understand it?
GitHub Actions can enforce this automatically:
name: Lint and Format
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install
- run: npm run lint
- run: npm run format
If the code doesn't pass linting, the check fails and the PR can't be approved. No manual reminder needed.
Common mistakes
Reviewing before the author is done. Some teams have a culture where junior developers request review as soon as they push code. That's fine, but reviewers should understand that early feedback is meant to be directional, not blocking. Don't treat a WIP PR like a finished change. If the description says "still working on tests," read the code and flag design issues, but don't require a rewrite before the author has had a chance to finish.
Leaving review comments instead of having a conversation. If you've left three comments on the same function and you're still not aligned, that's a signal to hop on a call. Asynchronous review works for small issues. For larger disagreements, talking it through is faster and kinder.
Approving code you don't understand. If a PR is in a codebase you're not familiar with, say so. Ask questions. Approve conditionally ("looks good, but I want to flag that I'm not an expert in this module—could [domain expert] take a look?"). Your signature matters. Use it carefully.
Ignoring edge cases and error handling. This is where reviews prevent production issues. Don't just check the happy path. Look at what happens when the database is slow, when an API is down, when the user inputs something unexpected. Ask about those scenarios in your review. It's the fastest way to catch bugs before they're live.
Key takeaways
- Start with design: ask if the approach makes sense before reviewing implementation details.
- Use GitHub's suggestion feature to make feedback actionable—one click is better than back-and-forth on how to write something.
- Explain why you're asking for a change and offer alternatives; this builds respect and moves the team forward.
- Automate formatting and style checks with linters and CI; save your review brain for the hard problems.
- Remember that code review is a conversation between professionals, not a checklist enforcement mechanism.






Comments
Loading comments…