The Debugging Mindset: A Systematic Approach to Finding Bugs
We've all been there: something's broken, you have no idea why, and you start making random changes hoping something will work. This is the path to madness (and confusing git histories).
Here's a better approach.
Step 1: Reproduce the Bug
If you can't reliably reproduce the bug, you can't fix it. You'll never know if your changes actually worked or if the bug just happens to be hiding.
Write down the exact steps to trigger the bug. Make them as minimal as possible:
1. Start the server
2. Navigate to /users/123
3. Click "Edit Profile"
4. Change email field
5. Click Save
→ Error: "Invalid token"
Step 2: Understand What Should Happen
Before you can fix what's wrong, you need to know what "right" looks like. What's the expected behavior? What's actually happening instead?
# Expected: Update user email and return success
# Actual: Throws "Invalid token" error
def update_user_email(user_id, new_email):
# What should this do?
# 1. Validate the email format
# 2. Check if email is already in use
# 3. Update database
# 4. Return success response
pass
Step 3: Form a Hypothesis
Based on the error message and the code, what's your best guess about what's going wrong?
"The 'Invalid token' error suggests the authentication middleware is rejecting the request. Maybe the token is expiring between page load and form submission?"
Step 4: Test Your Hypothesis
Don't just fix what you think is wrong. Verify your hypothesis first:
# Add logging to test hypothesis
def update_user_email(user_id, new_email):
token = request.headers.get('Authorization')
print(f"Token received: {token}") # Debug log
print(f"Token valid: {validate_token(token)}") # Debug log
# ... rest of function
If your hypothesis is wrong, you'll know immediately instead of chasing the wrong problem.
Step 5: Binary Search the Problem
If the codebase is large, narrow down where the bug lives:
- Add checkpoints: Put print statements or breakpoints at different stages
- Find the transition: Where does the data go from correct to incorrect?
- Zoom in: Focus on the code between the last good checkpoint and first bad one
function processData(input) {
console.log('Input:', input); // Checkpoint 1
const cleaned = cleanData(input);
console.log('Cleaned:', cleaned); // Checkpoint 2
const transformed = transform(cleaned);
console.log('Transformed:', transformed); // Checkpoint 3
return validate(transformed);
}
Step 6: Fix One Thing at a Time
When you find the issue, resist the urge to fix multiple problems at once. Change one thing, test if it works, then move to the next issue.
This way, when something breaks (and it will), you know exactly which change caused it.
The Rubber Duck Method
When all else fails, explain the problem out loud to someone (or something—a rubber duck works great). Often, the act of articulating the problem reveals the solution.
"So when the user clicks save, it sends a POST request to /api/users/update, which calls the updateUser function, which... wait, why is it calling the old API endpoint? I updated the backend but forgot to update the frontend!"
Debugging Is a Skill
Good debugging isn't about being clever or knowing every quirk of your language. It's about being systematic, patient, and methodical.
The best debuggers I know don't have some magical intuition. They just have a process they follow every time, and they trust it to eventually lead them to the answer.