Before you share code, review it as if you were the person who has to maintain it six months from now. A short, deliberate self-review catches confusing assumptions, accidental changes, edge cases, and avoidable review comments before they reach someone else.
1. Create distance before reviewing
The first review is more effective when you stop thinking like the author. If possible, finish the change, take a break, and return to it later. Even ten minutes away from the editor can help you notice structure and wording that felt obvious while you were writing.
Before you begin, define what the change is supposed to accomplish. Write down:
- The user or system problem being solved
- The expected behavior before and after the change
- Files or components that should be affected
- Important constraints, such as performance, compatibility, security, or accessibility
- Anything intentionally left out of scope
Then review the change from a clean comparison rather than scanning only the lines you remember editing. Use your version-control diff, preferably with whitespace changes hidden first and then shown separately if needed.
A useful self-review has two passes:
- Behavior pass: Does the code do the right thing in normal and unusual situations?
- Communication pass: Can another developer understand why it exists and safely modify it?
Keeping these passes separate prevents formatting preferences from distracting you from functional problems.
2. Start with the diff and the change boundary
Open the complete diff and look at it from top to bottom. Do not rely on the editor’s list of modified files alone. A diff reveals changes that are easy to overlook, including generated files, configuration updates, debug output, renamed functions, and unrelated formatting.
For every changed file, ask:
- Is this file actually necessary for the requested behavior?
- Did I modify more lines than needed?
- Did an automatic formatter or IDE change unrelated code?
- Are any new files missing from the diff?
- Did I accidentally alter a lockfile, environment setting, migration, or build configuration?
- Does the code belong in this module, or am I placing logic where it is merely convenient?
Check the working tree for untracked files and local changes that will not be included when you share the work. Conversely, make sure secrets, local settings, credentials, database dumps, and build artifacts are not included accidentally.
A narrow change is usually easier to review and less risky to merge. If you discover an unrelated cleanup while reviewing, record it separately instead of expanding the current change unless it is required for correctness.
3. Trace the behavior from input to output
Read the new or modified path as a user request would travel through the system. Identify the input, validation, transformation, storage or external call, and final output. Follow the actual control flow rather than assuming function names tell the whole story.
For each important path, answer:
- What starts this code?
- What inputs can arrive here?
- Which values are trusted, and which come from users, files, APIs, or databases?
- What happens when the input is empty, malformed, duplicated, or unexpectedly large?
- What does the caller expect in return?
- What happens after an error?
- Is the operation safe to repeat?
A compact behavior map can make gaps obvious:
| Stage | Review question | Common failure |
|---|---|---|
| Input | Can the value be missing or invalid? | Null, empty, or malformed data crashes the path |
| Validation | Are all required rules enforced? | Invalid data reaches business logic |
| Processing | Are assumptions explicit? | Wrong ordering, units, or state transition |
| Side effect | Can it fail or run twice? | Duplicate writes or partial updates |
| Output | Does the caller get a clear result? | Misleading success or swallowed error |
Pay particular attention to branches. Code that works for the expected case can still fail when a condition is false, a collection is empty, a record is missing, or an external service returns a delayed response. Ask what each branch means, not merely whether each branch is syntactically covered.
4. Review correctness and edge cases
After understanding the main path, challenge the assumptions behind it. Imagine inputs that are technically valid but uncommon. Typical edge cases include:
- Empty strings, empty arrays, and missing object properties
- Zero, negative, maximum, or extremely large numeric values
- Duplicate identifiers or repeated requests
- Different time zones, daylight-saving changes, and date boundaries
- Unicode characters, unusual capitalization, and long text
- Deleted or concurrently modified records
- Network timeouts, rate limits, partial responses, and retries
- Users with different permissions or accounts
- First-run behavior when a cache, table, directory, or configuration does not exist
Check boundaries explicitly. If a rule says “up to 10 items,” determine whether 10 is accepted and 11 is rejected. If a date range is inclusive, verify both endpoints. If an index, page number, or offset can be zero, confirm the condition does not treat zero as missing.
Also inspect error handling. A try block that catches every exception may keep the application running while hiding a serious defect. Determine whether the error is logged with enough context, returned to the caller, retried safely, or intentionally converted into a user-friendly response.
When you find a questionable case, decide whether to fix it, document it, or leave it outside the feature’s scope. Do not silently rely on a future reviewer to infer that decision.
5. Check readability and intent
Readable code is not merely code with a particular style. It is code that communicates decisions without forcing readers to reconstruct them. Review names, structure, and comments with that standard.
Look for:
- Names that describe what a value represents, not just its type
- Functions that have one clear responsibility
- Conditions that can be understood without mentally evaluating several negations
- Repeated logic that should be extracted—or repetition that is clearer than premature abstraction
- Comments that explain reasoning, constraints, or surprising behavior
- Comments that simply restate the code and will become stale
- Constants for domain rules that would otherwise appear as unexplained literals
- Consistent terminology across variables, functions, UI text, and documentation
Try explaining each non-obvious section in one sentence. If you cannot, the code may need a clearer structure or a short rationale. If the only explanation is a long comment describing tangled logic, simplify the logic where practical.
Avoid optimizing for cleverness. A compact expression is not an improvement if a teammate must test several mental cases to understand it. Prefer a straightforward implementation unless the more complex approach provides a measurable or necessary benefit.
6. Inspect security, privacy, and failure boundaries
Self-review should include a deliberate security pass, especially when code handles user input, authentication, payments, files, or external services.
Verify that:
- User-controlled values are validated and safely encoded for their destination
- Database queries use the application’s safe parameterization mechanism
- Authorization is checked on the server or trusted boundary, not only in the interface
- Sensitive data is not written to logs, error messages, URLs, or analytics events
- Secrets come from an approved configuration mechanism and are not committed
- File paths, uploads, redirects, and commands cannot be controlled unexpectedly
- Rate limits and resource limits exist where abuse could be costly
- Errors do not reveal credentials, internal paths, tokens, or private records
Review privacy as well as security. Ask whether the change collects, stores, or exposes more personal data than necessary. Confirm that test data and screenshots do not contain real credentials or identifiable information.
For code involving permissions, impersonate different roles mentally. A regular user, an administrator, an unauthenticated visitor, and an account viewing another account’s resource may all reach different outcomes. Verify that the outcome is intentional in each case.
7. Confirm tests and choose the right level
Run the checks that match the change, but do not treat a green test suite as proof that the code is correct. Tests may not cover a new branch, may encode the wrong expectation, or may omit important integration behavior.
Review existing tests before writing or changing them. They show the project’s conventions and reveal what maintainers consider part of the public behavior. For a new change, consider several layers:
- Unit checks for focused rules and transformations
- Integration checks for database, filesystem, queue, or service boundaries
- End-to-end checks for critical user journeys
- Manual checks for visual, usability, timing, or environment-specific behavior
At minimum, verify the normal path, an invalid input, a boundary condition, and the most likely failure from an external dependency. If adding a test would be disproportionate, document how you verified the behavior and what remains unverified.
Do not weaken a test merely to make the suite pass. If behavior intentionally changes, update the test and explain the reason in the change description. If a test is flaky, identify whether the problem is timing, shared state, network dependence, or an incorrect assumption before ignoring it.
8. Review performance and operational impact
Not every change needs optimization, but every change deserves a quick impact check. Look for loops that perform database or network requests repeatedly, loading large collections into memory, unnecessary serialization, repeated computation, and operations that grow more expensive as data grows.
Ask:
- What is the expected input size now, and what might it become later?
- Does this add a query, request, lock, or background job per item?
- Could caching return stale or unauthorized data?
- Does a retry amplify load or duplicate side effects?
- What happens if the operation runs concurrently?
- Will logs, metrics, or alerts become too noisy?
Consider operational behavior too. A useful change should be deployable, observable, and reversible when appropriate. Check migrations, feature flags, configuration defaults, backward compatibility, and whether old clients can interact with the new behavior.
If performance is uncertain, avoid making unsupported claims. State the concern and identify the measurement that would resolve it, such as a query plan, benchmark, trace, or production metric.
9. Make the code easy to review and share
Before sending the change, improve the reviewer’s experience. Remove temporary comments, debug statements, unused imports, dead code, and accidental formatting. Make sure the final diff is in a logical order where possible.
Your description should briefly explain:
- What changed
- Why it changed
- How you verified it
- Any known limitations or follow-up work
- Anything the reviewer should pay particular attention to
If the change is large, divide it into meaningful commits or smaller reviewable pieces when the project workflow allows. Avoid hiding important behavior inside a broad “cleanup” commit.
A useful final checklist is:
- The diff contains only intended changes.
- The code handles normal, invalid, empty, and boundary inputs.
- Errors are visible, useful, and safe.
- Authorization and sensitive data handling were checked.
- Relevant automated and manual verification was completed.
- Names and structure explain the intent.
- Documentation or configuration was updated where necessary.
- Known limitations are stated plainly.
Troubleshooting common self-review problems
You cannot understand the code anymore. Stop optimizing locally and reconstruct the feature from its entry point. Rename misleading variables, split large functions, and write down the missing invariant. If the design still feels unclear, ask for a small design review before polishing details.
The diff is too large to review. Separate formatting from behavior, revert unrelated cleanup from the current branch, and review one logical area at a time. Large diffs often indicate that the task boundary was not defined early enough.
Tests pass, but the behavior feels risky. Identify the untested assumption instead of adding random tests. Add a focused case or perform a targeted manual check at the relevant boundary, then record what the existing suite does not cover.
You are unsure whether to fix an edge case. Estimate its likelihood, impact, and cost. Fix it now when the correction is small or the failure affects security, data integrity, or a common path. Otherwise document the limitation and create a focused follow-up.
You keep finding new improvements. Separate required correctness fixes from optional refactoring. Share the required change first, and track unrelated improvements so they do not obscure the purpose of the review.
Limitations of self-review
Self-review cannot replace another person entirely. Authors are naturally attached to their assumptions, and they may overlook domain rules, accessibility concerns, unfamiliar failure modes, or risks that are obvious to a fresh reader. A self-review also cannot prove performance, security, or reliability without appropriate measurement and specialist attention.
Use this process to raise the quality of what you share, not to avoid collaboration. For high-risk changes—authentication, payments, data migrations, public APIs, infrastructure, or privacy-sensitive features—combine your review with targeted testing and review from someone who understands the relevant system. The goal is not to make the code perfect before anyone sees it; it is to make the intent, evidence, and remaining uncertainty clear enough for useful feedback.