I used to submit PRs the moment my code worked. Hit "Create Pull Request" and wait for feedback.
The result:
- First review: "You left console.log statements"
- Second review: "Tests are failing"
- Third review: "This variable name is misleading"
- Fourth review: "You forgot to update the documentation"
- Fifth review: Finally approved
Five review cycles. Three days. Multiple reviewers annoyed.
Then I learned to self-review. Now:
- One review cycle
- Same day approval
- Reviewers actually thank me for thorough PRs
The secret: Spend 10 minutes reviewing your own code before anyone else sees it.
Here's exactly how I do it.
Why Self-Review Matters
The Embarrassment Factor
Nothing feels worse than:
- Reviewer pointing out you left
console.log('DEBUG: user data:', user) - Finding out tests are failing (didn't run them locally)
- Realizing you committed commented-out code
- Someone asking "What does
temp2mean?"
Self-review eliminates 80% of embarrassing feedback.
The Time Factor
Without self-review:
Submit PR → Wait 4 hours → Review feedback → Fix issues →
Wait 4 hours → More feedback → Fix more → Wait 4 hours → Approved
Total: 2-3 days, 3+ review cycles
With self-review:
Self-review 10 minutes → Submit clean PR → Wait 4 hours → Approved
Total: 4 hours, 1 review cycle
Time saved: 2+ days per PR
The Relationship Factor
Reviewers notice when you consistently submit clean PRs. They:
- Review your PRs faster
- Give more thoughtful feedback
- Trust your code more
- Recommend you for senior positions
Self-review is a career accelerator.
The 10-Minute Self-Review Process
Here's my exact process. Takes 10 minutes. Saves hours.
Step 1: Wait 5 Minutes (Seriously)
Before reviewing, take a break:
- Get coffee
- Walk around
- Check messages
- Stretch
Why: Fresh eyes catch things you miss when you're "in the zone."
Your brain was in "make it work" mode. You need "does this make sense?" mode.
Step 2: Review on GitHub/GitLab (Not Your IDE)
Don't review in VS Code. Review in GitHub.
Why:
- ✅ Same view reviewers will see
- ✅ Catches formatting issues
- ✅ Forces you to read like a reviewer
- ❌ IDE familiarity makes you blind to issues
Action:
- Create draft PR
- Click "Files changed"
- Review every file as if you're the reviewer
Step 3: Read Your Own Code Out Loud
Literally read your changes out loud (or silently mouth the words).
// Reading this out loud:
"Function process user takes user...
returns user dot data dot profile dot email...
Wait, what if profile is null?"
Catches: Logic errors, missing null checks, confusing code
Step 4: Run the Checklist (See Below)
Go through the complete checklist systematically.
Don't skip items. Each one catches real issues.
Step 5: Use AI to Review Your Code
Game changer: Let AI catch what you miss.
# Use Claude Code
git diff main | claude code --skill self-review-check
# Or paste into Claude/ChatGPT
"Review this diff for issues I might have missed:
- Console.logs
- TODO comments
- Commented code
- Poor variable names
- Missing tests
- Type safety issues
[paste diff]"
AI catches:
- Things you're blind to (console.logs you added while debugging)
- Patterns you don't notice (same logic in multiple files)
- Edge cases you didn't test
- Documentation gaps
Step 6: Test One More Time
Even if tests passed before:
# Clean install and test
rm -rf node_modules
npm install
npm test
npm run build
npm run lint
Why: Catches:
- Tests that only pass with cached dependencies
- Linting errors you ignored
- Build failures in production mode
- Type errors you suppressed locally
The Complete Self-Review Checklist
Copy this. Use it every time.
Code Quality Check
Basic Cleanup:
- [ ] No
console.logstatements (uselogger.debuginstead) - [ ] No commented-out code (delete it, it's in git history)
- [ ] No TODO comments (create GitHub issues instead)
- [ ] No temporary test data (hardcoded values)
- [ ] No "temp" or "test" variable names
- [ ] No debug code left in
Code Style:
- [ ] Consistent naming conventions (camelCase, PascalCase)
- [ ] No single-letter variables (except loop counters)
- [ ] Functions are small and focused (< 50 lines)
- [ ] No deeply nested logic (> 3 levels)
- [ ] Clear function and variable names
- [ ] Code is formatted (Prettier/ESLint)
Example - Before self-review:
// ❌ This would embarrass you in review
function processStuff(data: any) {
console.log('DEBUG:', data); // Left in
const temp = data.users; // Vague name
// const old_code = temp.filter(x => x.active); // Commented out
// TODO: optimize this later
for(let i=0; i<temp.length; i++) {
for(let j=0; j<temp[i].posts.length; j++) {
for(let k=0; k<temp[i].posts[j].comments.length; k++) {
// Nested 3 levels deep
if(temp[i].posts[j].comments[k].flagged) {
console.log('found one!');
}
}
}
}
}
After self-review:
// ✅ Clean, professional
function getFlaggedComments(users: User[]): Comment[] {
return users.flatMap(user =>
user.posts.flatMap(post =>
post.comments.filter(comment => comment.flagged)
)
);
}
TypeScript Safety Check
Type Safety:
- [ ] No
anytypes (or justified with comment) - [ ] No
@ts-ignore(or explained why needed) - [ ] No
@ts-expect-errorwithout TODO issue - [ ] Type assertions are justified
- [ ] All function parameters are typed
- [ ] Return types are explicit for public APIs
- [ ] Null/undefined handled properly
- [ ] No implicit
anyfrom missing types
Type-Specific Issues:
- [ ] Optional chaining used (
?.) where needed - [ ] Nullish coalescing used (
??) instead of|| - [ ] Type imports use
import typesyntax - [ ] Discriminated unions for state machines
- [ ] Generic constraints are correct
- [ ] No unsafe type assertions
Example - Common type issues:
// ❌ Would get flagged in review
function getUser(id: string) { // Missing return type
const user = users.find(u => u.id === id)!; // Unsafe !
return user.profile.email; // What if profile is null?
}
const data: any = await response.json(); // any!
// ✅ After self-review
function getUser(id: string): string | null {
const user = users.find(u => u.id === id);
return user?.profile?.email ?? null;
}
const UserSchema = z.object({ /* ... */ });
const data = UserSchema.parse(await response.json());
React Patterns Check
Hooks:
- [ ] Hooks follow rules (no conditional hooks)
- [ ] useEffect dependencies are correct
- [ ] No missing dependencies (ESLint warning)
- [ ] Cleanup functions for subscriptions
- [ ] No infinite loops (missing deps causing re-runs)
Performance:
- [ ] Expensive calculations use
useMemo - [ ] Callback functions use
useCallback - [ ] Heavy components use
React.memo - [ ] No unnecessary object/array creation in render
- [ ] Large lists are virtualized
State Management:
- [ ] State is not derived from props (unless intentional)
- [ ] No direct state mutation
- [ ] State updates use functional form when needed
- [ ] Complex state uses
useReducer
Example - Common React issues:
// ❌ Would get flagged
function Component({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Missing userId dependency!
const config = { theme: 'dark' }; // New object every render
return <Child config={config} />; // Child re-renders unnecessarily
}
// ✅ After self-review
function Component({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Correct dependencies
const config = useMemo(() => ({ theme: 'dark' }), []); // Memoized
return <Child config={config} />;
}
Testing Check
Test Coverage:
- [ ] Unit tests for new functions
- [ ] Integration tests for new features
- [ ] Tests actually test something (not just smoke tests)
- [ ] Edge cases are tested
- [ ] Error cases are tested
- [ ] Tests have descriptive names
Test Quality:
- [ ] No commented-out tests
- [ ] No
.onlyor.skipleft in - [ ] Tests are independent (no shared state)
- [ ] Mock data is realistic
- [ ] Assertions are specific (not just "exists")
Test Execution:
- [ ] All tests pass locally
- [ ] Tests pass consistently (not flaky)
- [ ] Coverage didn't decrease
- [ ] Tests run in reasonable time
Example - Test issues to catch:
// ❌ Would get flagged
describe.only('UserService', () => { // Left .only in!
it('works', () => { // Vague description
const result = getUser('1');
expect(result).toBeTruthy(); // Weak assertion
});
// it('handles errors', () => {}); // Commented out test
});
// ✅ After self-review
describe('UserService', () => {
it('returns user when ID exists', () => {
const result = getUser('1');
expect(result).toEqual({
id: '1',
name: 'Test User',
email: 'test@example.com'
});
});
it('returns null when ID does not exist', () => {
const result = getUser('nonexistent');
expect(result).toBeNull();
});
it('throws error when ID is invalid format', () => {
expect(() => getUser('')).toThrow('Invalid user ID');
});
});
Performance Check
Bundle Size:
- [ ] No large library imports (import only what you need)
- [ ] Heavy components are code-split
- [ ] Images are optimized
- [ ] No duplicate dependencies
Runtime Performance:
- [ ] No expensive operations in render
- [ ] API calls are debounced/throttled
- [ ] Large lists use virtualization
- [ ] No memory leaks (listeners cleaned up)
Network:
- [ ] API calls are batched where possible
- [ ] Requests can be cancelled
- [ ] Data is cached appropriately
- [ ] No waterfall requests (parallel when possible)
Example - Performance issues:
// ❌ Would slow down app
import _ from 'lodash'; // Imports entire lodash!
function Component({ items }) {
const sorted = items
.filter(i => i.active)
.sort((a, b) => b.date - a.date); // Runs every render!
return <List items={sorted} />;
}
// ✅ After self-review
import sortBy from 'lodash/sortBy'; // Import only what's needed
function Component({ items }) {
const sorted = useMemo(
() => items
.filter(i => i.active)
.sort((a, b) => b.date - a.date),
[items] // Only re-sort when items change
);
return <List items={sorted} />;
}
Security Check
Sensitive Data:
- [ ] No API keys in code (use env variables)
- [ ] No passwords or secrets hardcoded
- [ ] No sensitive data in logs
- [ ] No sensitive data in error messages
Input Validation:
- [ ] User input is validated
- [ ] User input is sanitized
- [ ] No SQL injection risk
- [ ] No XSS vulnerabilities
Authentication/Authorization:
- [ ] Auth checks are in place
- [ ] Tokens are stored securely
- [ ] Sessions are handled properly
- [ ] CSRF protection exists
Example - Security issues:
// ❌ Security issues
const API_KEY = 'sk-1234567890'; // Hardcoded!
function searchUsers(query: string) {
// SQL injection risk!
const sql = `SELECT * FROM users WHERE name = '${query}'`;
return db.query(sql);
}
function renderComment(comment: string) {
// XSS vulnerability!
return <div dangerouslySetInnerHTML={{ __html: comment }} />;
}
console.log('User password:', password); // Logging sensitive data!
// ✅ After self-review
const API_KEY = process.env.REACT_APP_API_KEY; // From env
function searchUsers(query: string) {
// Parameterized query
return db.query('SELECT * FROM users WHERE name = ?', [query]);
}
function renderComment(comment: string) {
// React escapes by default
return <div>{comment}</div>;
}
logger.info('User logged in', { userId: user.id }); // No sensitive data
Accessibility Check
Semantic HTML:
- [ ] Using semantic elements (, , )
- [ ] Proper heading hierarchy
- [ ] Lists use
- /
- [ ] Forms use element
ARIA:
- [ ] ARIA labels on icon-only buttons
- [ ] ARIA live regions for dynamic content
- [ ] ARIA states (expanded, selected, etc.)
- [ ] No redundant ARIA
Keyboard Navigation:
- [ ] All interactive elements focusable
- [ ] Tab order is logical
- [ ] Focus visible on all elements
- [ ] Keyboard shortcuts don't conflict
Screen Readers:
- [ ] Images have alt text
- [ ] Form labels are associated
- [ ] Error messages are announced
- [ ] Loading states are announced
Example - Accessibility issues:
// ❌ Accessibility issues
<div onClick={handleClick}>Click me</div> // Not keyboard accessible
<button onClick={handleClose}>
<X /> {/* Icon only, no label */}
</button>
<img src="profile.jpg" /> {/* No alt text */}
<input type="text" /> {/* No label */}
// ✅ After self-review
<button onClick={handleClick}>Click me</button>
<button onClick={handleClose} aria-label="Close dialog">
<X />
</button>
<img src="profile.jpg" alt="User profile picture" />
<label htmlFor="email">Email</label>
<input type="text" id="email" />
Documentation Check
Code Comments:
- [ ] Complex logic is explained
- [ ] Non-obvious decisions are documented
- [ ] Workarounds have explanations
- [ ] Public APIs are documented
README Updates:
- [ ] New features documented
- [ ] Breaking changes noted
- [ ] Examples updated
- [ ] Installation steps current
API Documentation:
- [ ] New endpoints documented
- [ ] Request/response examples provided
- [ ] Error codes documented
- [ ] Rate limits noted
Example - Documentation issues:
// ❌ No context for future developers
function calculateScore(a, b, c) {
return (a * 0.4) + (b * 0.35) + (c * 0.25); // Magic numbers!
}
// ✅ After self-review
/**
* Calculates user engagement score based on weighted metrics.
*
* @param posts - Number of posts (40% weight)
* @param comments - Number of comments (35% weight)
* @param reactions - Number of reactions received (25% weight)
* @returns Score from 0-100
*
* Weights determined by engagement analysis in Q3 2024.
* See: https://docs.company.com/engagement-scoring
*/
function calculateEngagementScore(
posts: number,
comments: number,
reactions: number
): number {
const POST_WEIGHT = 0.4;
const COMMENT_WEIGHT = 0.35;
const REACTION_WEIGHT = 0.25;
return (posts * POST_WEIGHT) +
(comments * COMMENT_WEIGHT) +
(reactions * REACTION_WEIGHT);
}
Git Hygiene Check
Commits:
- [ ] Commit messages are clear
- [ ] No "WIP" or "temp" commits
- [ ] Commits are logical chunks
- [ ] No merge commits (rebased)
Branch:
- [ ] Branch is up to date with main
- [ ] No merge conflicts
- [ ] Branch name is descriptive
Files:
- [ ] No unnecessary files committed
- [ ] No .env files or secrets
- [ ] No large binary files (unless necessary)
- [ ] .gitignore is updated
Example - Git issues:
# ❌ Bad commit history
git log
> WIP
> more stuff
> fixes
> actually works now
> Merge branch 'main'
> temp
# ✅ After self-review cleanup
git rebase -i main
# Squash into meaningful commits:
> Add user profile validation
> Fix null pointer in profile fetch
> Update tests for new validation
Common Things Developers Miss
1. Console.log Statements
Everyone does this. You add debugging, code works, you forget to remove it.
Self-review catch:
# Search before submitting
git diff main | grep -i console
# Or use pre-commit hook
2. Commented-Out Code
"I'll uncomment it later" - No you won't. Delete it.
// ❌ Looks unprofessional
function process() {
// const oldWay = data.map(x => x.value);
// const anotherOldWay = data.filter(x => x.active);
const newWay = data.filter(x => x.active).map(x => x.value);
return newWay;
}
// ✅ Clean
function process() {
return data.filter(x => x.active).map(x => x.value);
}
It's in git history if you need it.
3. TODO Comments
Create a GitHub issue instead.
// ❌ Will never get done
function uploadFile() {
// TODO: add validation
// TODO: handle large files
// TODO: add progress indicator
upload(file);
}
// ✅ Track properly
function uploadFile() {
// FIXME: Add file validation (Issue #1234)
// FIXME: Handle large files >100MB (Issue #1235)
upload(file);
}
4. Poor Variable Names
You know what temp2 means now. Will you in 6 months?
// ❌ Future you will hate this
const temp = data.filter(x => x.active);
const temp2 = temp.map(x => x.value);
const result = temp2.sort();
// ✅ Self-documenting
const activeItems = data.filter(item => item.active);
const itemValues = activeItems.map(item => item.value);
const sortedValues = itemValues.sort();
// ✅ Even better - one clear chain
const sortedActiveValues = data
.filter(item => item.active)
.map(item => item.value)
.sort();
5. Missing Null Checks
"It should never be null" - Famous last words.
// ❌ Will crash eventually
function getUserEmail(userId: string): string {
const user = users.find(u => u.id === userId);
return user.profile.email; // What if user not found?
}
// ✅ Defensive
function getUserEmail(userId: string): string | null {
const user = users.find(u => u.id === userId);
if (!user) return null;
return user.profile?.email ?? null;
}
6. Unhandled Promises
Silent failures are the worst bugs.
// ❌ Error disappears
useEffect(() => {
fetchData(); // Promise rejection unhandled!
}, []);
// ✅ Proper error handling
useEffect(() => {
fetchData().catch(error => {
console.error('Failed to fetch:', error);
setError(error);
});
}, []);
7. Overly Complex Logic
If you need to re-read your own code, it's too complex.
// ❌ What does this even do?
const x = a.map(b => b.c).filter((d, e) =>
d && d.f && d.f.g ? d.f.g.h === i[e].j : false
).reduce((k, l) => k ? {...k, [l.m]: l.n} : {}, {});
// ✅ Broken into steps
const userProfiles = users.map(user => user.profile);
const validProfiles = userProfiles.filter(
(profile, index) => {
if (!profile?.settings?.preferences) return false;
return profile.settings.preferences.theme === themes[index].default;
}
);
const preferencesMap = validProfiles.reduce(
(map, profile) => ({
...map,
[profile.id]: profile.preferences
}),
{}
);
8. Copy-Paste Without Updating
Classic mistake: Copy function, forget to update names/comments.
// ❌ Copied and pasted, didn't update
/**
* Gets user by ID
*/
function getPost(id: string): Post {
return posts.find(p => p.id === id); // Wrong comment!
}
// ✅ Updated properly
/**
* Gets post by ID
*/
function getPost(id: string): Post | undefined {
return posts.find(post => post.id === id);
}
9. Hardcoded Test Data
Remove your test values before committing.
// ❌ Test data in production code
function Component() {
const [email, setEmail] = useState('test@test.com');
const [password, setPassword] = useState('password123');
// ...
}
// ✅ Clean
function Component() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// ...
}
10. Incomplete Cleanup
If you tried something and reverted, clean up completely.
// ❌ Leftovers from experimentation
import { OldComponent } from './old'; // Not used anymore
import { NewComponent } from './new';
interface OldProps { /* ... */ } // Not used
function Component() {
// const [oldState, setOldState] = useState(); // Commented out
const [newState, setNewState] = useState();
return <NewComponent state={newState} />;
}
// ✅ Clean
import { NewComponent } from './new';
function Component() {
const [state, setState] = useState();
return <NewComponent state={state} />;
}
Tools for Self-Review
1. Claude Code Skills for Self-Review
Create .claude/skills/self-review.md:
# Self-Review Checker
You are reviewing code before it gets submitted for PR review.
## Your Task
Analyze this git diff and flag issues the developer likely missed.
## Check For
**Obvious Mistakes:**
- console.log statements
- console.error, console.warn, console.debug
- debugger statements
- Commented-out code
- TODO/FIXME comments without issue numbers
- "temp", "test", "debug" variable names
- WIP or test code
**Type Safety:**
- Explicit `any` types
- `@ts-ignore` or `@ts-expect-error`
- Unsafe non-null assertions (!)
- Missing null checks
- Type assertions without validation
**Code Quality:**
- Functions longer than 50 lines
- Nested logic deeper than 3 levels
- Duplicate code
- Magic numbers without constants
- Poor variable names
**React-Specific:**
- Missing useEffect dependencies
- Expensive operations not memoized
- Unnecessary re-renders
- Missing cleanup functions
**Performance:**
- Large library imports
- Unoptimized images
- Synchronous expensive operations
**Security:**
- Hardcoded secrets or API keys
- Sensitive data in logs
- Unvalidated user input
- Potential XSS
**Testing:**
- `.only` or `.skip` in tests
- Commented-out tests
- Tests without assertions
**Git Hygiene:**
- Large binary files
- Committed .env files
- Uncommitted package-lock.json changes
## Output Format
### 🔴 Critical Issues (Fix before submitting)
[List with file:line and explanation]
### 🟡 Warnings (Should fix)
[List with file:line and explanation]
### 🟢 Suggestions (Nice to have)
[List with file:line and explanation]
### ✅ Looks Good
[What looks good in the PR]
## Be Specific
Include file names and line numbers.
Explain WHY it's an issue.
Suggest a fix.
Usage:
# Before submitting PR
git diff main | claude code --skill self-review
Result: AI catches what you're blind to.
2. GitHub Copilot for Quick Checks
In VS Code:
@workspace Review my staged changes for common issues:
- Debugging code left in
- Type safety issues
- Missing error handling
- Poor naming
Copilot will scan and flag issues.
3. ESLint + TypeScript
Essential setup:
npm install --save-dev \
eslint \
@typescript-eslint/eslint-plugin \
@typescript-eslint/parser
Run before submitting:
npm run lint
Pre-commit hook:
{
"husky": {
"hooks": {
"pre-commit": "npm run lint && npm test"
}
}
}
4. Pretty Quick
Format only changed files:
npm install --save-dev pretty-quick
# Add to package.json
{
"scripts": {
"format": "pretty-quick --staged"
}
}
Pre-commit hook:
{
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged && npm test"
}
}
}
5. Danger.js
Automated self-review checks:
// dangerfile.js
import { danger, warn, fail, message } from 'danger';
// Check for console.log
const consoleStatements = danger.git.modified_files
.filter(file => file.endsWith('.ts') || file.endsWith('.tsx'))
.map(file => {
const content = danger.git.fileMatch(file);
// Check for console statements
});
if (consoleStatements.length > 0) {
fail('console.log found. Remove debugging code.');
}
// Check for large PRs
const bigPR = danger.github.pr.additions + danger.github.pr.deletions > 500;
if (bigPR) {
warn('This PR is large. Consider splitting it.');
}
// Check for missing tests
const hasAppChanges = danger.git.modified_files.some(
f => f.match(/src\//) && f.endsWith('.tsx')
);
const hasTestChanges = danger.git.modified_files.some(
f => f.includes('test')
);
if (hasAppChanges && !hasTestChanges) {
warn('No tests added. Consider adding tests.');
}
// Require PR description
if (danger.github.pr.body.length < 50) {
fail('Please add a proper PR description.');
}
// Check for TODOs
const newTodos = danger.git.modified_files.filter(file => {
const content = danger.git.fileMatch(file);
// Check for TODO without issue number
});
if (newTodos.length > 0) {
warn('TODO comments found. Create GitHub issues instead.');
}
6. Git Hooks with Husky
Comprehensive pre-commit checks:
npm install --save-dev husky lint-staged
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{ts,tsx}": [
"bash -c 'npm test -- --findRelatedTests'"
]
}
}
7. VS Code Extensions
Install these:
- ESLint - Real-time linting
- Prettier - Auto-formatting
- Error Lens - Inline error messages
- Code Spell Checker - Catch typos
- GitLens - See git blame inline
- SonarLint - Security and code smell detection
Settings:
// .vscode/settings.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}
8. Bundle Analyzer
Check bundle size impact:
npm install --save-dev webpack-bundle-analyzer
# Add to package.json
{
"scripts": {
"analyze": "webpack-bundle-analyzer stats.json"
}
}
Run before submitting:
npm run build
npm run analyze
Flag if you've increased bundle size significantly.
The 5-Minute Self-Review Workflow
Here's my exact 5-minute process:
Minute 1: Quick Scan
# Check for obvious issues
git diff main | grep -i "console\."
git diff main | grep -i "TODO"
git diff main | grep -i "FIXME"
git diff main | grep -i "debugger"
git diff main | grep "\.only\|\.skip"
Minute 2: AI Review
# Let AI catch what you missed
git diff main | claude code --skill self-review > review.md
cat review.md
Fix critical issues flagged by AI.
Minute 3: Test & Lint
npm run lint
npm test
npm run build
All must pass.
Minute 4: Visual Review on GitHub
- Create draft PR
- Go to "Files changed"
- Review each file as if you're the reviewer
- Look for issues the tools missed
Minute 5: Final Checks
- [ ] PR description complete
- [ ] Screenshots added (if UI change)
- [ ] Tests added
- [ ] Documentation updated
- [ ] Ready to convert to real PR
Reducing Review Cycles
Track Your Improvement
Week 1: Count review cycles per PR
Week 2: Implement self-review checklist
Week 3: Compare review cycles
My results:
- Before: Average 3.2 review cycles per PR
- After: Average 1.1 review cycles per PR
- Time saved: ~2 days per PR
Make It a Habit
Triggers to start self-review:
- Tests pass locally → Self-review
- About to push → Self-review
- Creating PR → Self-review draft first
Don't submit without self-review. Ever.
Common Objections
"I don't have time for self-review"
Self-review takes 10 minutes.
Each review cycle takes 4+ hours (waiting).
You'll save time.
"The reviewer will catch it anyway"
True, but:
- They'll be annoyed
- It wastes their time
- It slows down your PR
- It hurts your reputation
"I already reviewed it while coding"
No you didn't. You were focused on making it work.
Self-review is different - you're looking for issues.
Real Examples: Before and After Self-Review
Example 1: The Debugging Left Behind
Before self-review (would submit this):
function processOrder(order: Order) {
console.log('Processing order:', order); // Debug
console.log('User ID:', order.userId); // Debug
const user = getUser(order.userId);
console.log('User found:', user); // Debug
if (!user) {
console.error('User not found!'); // Debug
return null;
}
console.log('Calculating total...'); // Debug
const total = calculateTotal(order);
console.log('Total:', total); // Debug
return total;
}
After self-review:
function processOrder(order: Order): number | null {
const user = getUser(order.userId);
if (!user) {
logger.error('User not found', {
orderId: order.id,
userId: order.userId
});
return null;
}
return calculateTotal(order);
}
Caught: 7 debug console.logs, no return type
Example 2: The Unhandled Edge Cases
Before self-review:
function getUserName(userId: string): string {
const user = users.find(u => u.id === userId);
return user.name;
}
After self-review:
function getUserName(userId: string): string {
const user = users.find(u => u.id === userId);
if (!user) {
logger.warn('User not found', { userId });
return 'Unknown User';
}
return user.name ?? 'Unnamed User';
}
Caught: No null check, no handling of missing name
Example 3: The Performance Issue
Before self-review:
function Component({ items }: { items: Item[] }) {
const filtered = items.filter(i => i.active);
const sorted = filtered.sort((a, b) => b.date - a.date);
const formatted = sorted.map(i => formatItem(i));
return <List items={formatted} />;
}
After self-review:
function Component({ items }: { items: Item[] }) {
const processedItems = useMemo(
() => items
.filter(item => item.active)
.sort((a, b) => b.date - a.date)
.map(item => formatItem(item)),
[items]
);
return <List items={processedItems} />;
}
Caught: Expensive operations running every render
Example 4: The Type Safety Disaster
Before self-review:
function processData(data: any) {
const users = data.users;
const posts = data.posts;
return users.map(u => ({
...u,
posts: posts.filter(p => p.userId === u.id)
}));
}
After self-review:
interface User {
id: string;
name: string;
}
interface Post {
id: string;
userId: string;
title: string;
}
interface UserWithPosts extends User {
posts: Post[];
}
function processData(data: {
users: User[];
posts: Post[];
}): UserWithPosts[] {
const { users, posts } = data;
return users.map(user => ({
...user,
posts: posts.filter(post => post.userId === user.id)
}));
}
Caught: any type, no type safety, unclear return type
The Self-Review Mindset
Think Like a Reviewer
When self-reviewing, ask yourself:
- "Would I approve this PR?"
- "What questions would I ask?"
- "What would confuse me?"
- "What looks risky?"
- "What could break?"
Be Honest With Yourself
Don't rationalize issues:
❌ "I'll fix that TODO later"
❌ "That console.log won't hurt"
❌ "Good enough for now"
❌ "The reviewer won't notice"
✅ "I should fix this now"
✅ "I should remove that"
✅ "I should make this clearer"
✅ "I should add a test"
Take Pride in Clean PRs
Your PR is a reflection of your professionalism.
Clean PRs show:
- You care about quality
- You respect reviewers' time
- You think about maintainability
- You're ready for senior roles
Measuring Success
Before Self-Review
Track for 2 weeks:
- Review cycles per PR
- Time from submit to approval
- Number of "obvious" issues found by reviewers
- Reviewer frustration (complaints about missed things)
After Self-Review
Track for 2 weeks:
- Review cycles per PR (should drop 50%+)
- Time from submit to approval (should drop 50%+)
- "Obvious" issues (should drop 80%+)
- Reviewer happiness (compliments on clean PRs)
My Results
Before:
- Average review cycles: 3.2
- Average time to approval: 2.3 days
- Console.logs found by reviewers: 12/month
- Test failures caught in review: 8/month
After:
- Average review cycles: 1.1
- Average time to approval: 0.5 days
- Console.logs found by reviewers: 1/month
- Test failures caught in review: 0/month
Time saved: ~40 hours/month
Conclusion
Self-review isn't extra work. It's the fastest way to ship code.
10 minutes of self-review saves:
- ✅ 2+ days of waiting
- ✅ Multiple review cycles
- ✅ Reviewer frustration
- ✅ Your reputation
- ✅ Embarrassing mistakes
The checklist:
- Code quality (no console.logs, TODOs, etc.)
- Type safety (no
any, proper null handling) - React patterns (hooks, performance)
- Testing (coverage, quality)
- Security (no secrets, validation)
- Accessibility (semantic HTML, ARIA)
- Documentation (comments, README)
- Git hygiene (clean commits)
The tools:
- Claude Code skills (catches what you're blind to)
- ESLint + TypeScript
- Pre-commit hooks
- AI review
The mindset:
- Think like a reviewer
- Be honest with yourself
- Take pride in clean code
Start tomorrow:
- Create draft PR
- Review on GitHub
- Run AI review:
git diff main | claude code --skill self-review - Fix all issues
- Submit clean PR
- Get approved faster
Your reviewers will notice. Your career will benefit.
Self-review is the fastest path to senior engineer.
What's your self-review process? Share your checklist!
Top comments (0)