DEV Community

Tarun Moorjani
Tarun Moorjani

Posted on

Self-Review: Catching Your Own Mistakes Before Others Do

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 temp2 mean?"

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
Enter fullscreen mode Exit fullscreen mode

With self-review:

Self-review 10 minutes → Submit clean PR → Wait 4 hours → Approved

Total: 4 hours, 1 review cycle
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create draft PR
  2. Click "Files changed"
  3. 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?"
Enter fullscreen mode Exit fullscreen mode

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]"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.log statements (use logger.debug instead)
  • [ ] 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!');
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

After self-review:

// ✅ Clean, professional
function getFlaggedComments(users: User[]): Comment[] {
  return users.flatMap(user =>
    user.posts.flatMap(post =>
      post.comments.filter(comment => comment.flagged)
    )
  );
}
Enter fullscreen mode Exit fullscreen mode

TypeScript Safety Check

Type Safety:

  • [ ] No any types (or justified with comment)
  • [ ] No @ts-ignore (or explained why needed)
  • [ ] No @ts-expect-error without TODO issue
  • [ ] Type assertions are justified
  • [ ] All function parameters are typed
  • [ ] Return types are explicit for public APIs
  • [ ] Null/undefined handled properly
  • [ ] No implicit any from missing types

Type-Specific Issues:

  • [ ] Optional chaining used (?.) where needed
  • [ ] Nullish coalescing used (??) instead of ||
  • [ ] Type imports use import type syntax
  • [ ] 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());
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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 .only or .skip left 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');
  });
});
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" />
    
    Enter fullscreen mode Exit fullscreen mode

    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);
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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
    
    Enter fullscreen mode Exit fullscreen mode

    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
    
    Enter fullscreen mode Exit fullscreen mode

    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);
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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);
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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();
    
    Enter fullscreen mode Exit fullscreen mode

    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;
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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);
      });
    }, []);
    
    Enter fullscreen mode Exit fullscreen mode

    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
      }),
      {}
    );
    
    Enter fullscreen mode Exit fullscreen mode

    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);
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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('');
      // ...
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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} />;
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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.
    
    Enter fullscreen mode Exit fullscreen mode

    Usage:

    # Before submitting PR
    git diff main | claude code --skill self-review
    
    Enter fullscreen mode Exit fullscreen mode

    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
    
    Enter fullscreen mode Exit fullscreen mode

    Copilot will scan and flag issues.

    3. ESLint + TypeScript

    Essential setup:

    npm install --save-dev \
      eslint \
      @typescript-eslint/eslint-plugin \
      @typescript-eslint/parser
    
    Enter fullscreen mode Exit fullscreen mode

    Run before submitting:

    npm run lint
    
    Enter fullscreen mode Exit fullscreen mode

    Pre-commit hook:

    {
      "husky": {
        "hooks": {
          "pre-commit": "npm run lint && npm test"
        }
      }
    }
    
    Enter fullscreen mode Exit fullscreen mode

    4. Pretty Quick

    Format only changed files:

    npm install --save-dev pretty-quick
    
    # Add to package.json
    {
      "scripts": {
        "format": "pretty-quick --staged"
      }
    }
    
    Enter fullscreen mode Exit fullscreen mode

    Pre-commit hook:

    {
      "husky": {
        "hooks": {
          "pre-commit": "pretty-quick --staged && npm test"
        }
      }
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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.');
    }
    
    Enter fullscreen mode Exit fullscreen mode

    6. Git Hooks with Husky

    Comprehensive pre-commit checks:

    npm install --save-dev husky lint-staged
    
    Enter fullscreen mode Exit fullscreen mode
    // 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'"
        ]
      }
    }
    
    Enter fullscreen mode Exit fullscreen mode

    7. VS Code Extensions

    Install these:

    1. ESLint - Real-time linting
    2. Prettier - Auto-formatting
    3. Error Lens - Inline error messages
    4. Code Spell Checker - Catch typos
    5. GitLens - See git blame inline
    6. 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"
      ]
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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"
      }
    }
    
    Enter fullscreen mode Exit fullscreen mode

    Run before submitting:

    npm run build
    npm run analyze
    
    Enter fullscreen mode Exit fullscreen mode

    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"
    
    Enter fullscreen mode Exit fullscreen mode

    Minute 2: AI Review

    # Let AI catch what you missed
    git diff main | claude code --skill self-review > review.md
    cat review.md
    
    Enter fullscreen mode Exit fullscreen mode

    Fix critical issues flagged by AI.

    Minute 3: Test & Lint

    npm run lint
    npm test
    npm run build
    
    Enter fullscreen mode Exit fullscreen mode

    All must pass.

    Minute 4: Visual Review on GitHub

    1. Create draft PR
    2. Go to "Files changed"
    3. Review each file as if you're the reviewer
    4. 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:

    1. Tests pass locally → Self-review
    2. About to push → Self-review
    3. 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;
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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);
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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;
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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';
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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} />;
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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} />;
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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)
      }));
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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)
      }));
    }
    
    Enter fullscreen mode Exit fullscreen mode

    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:

    1. Create draft PR
    2. Review on GitHub
    3. Run AI review: git diff main | claude code --skill self-review
    4. Fix all issues
    5. Submit clean PR
    6. 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)