| name | debug-systematically |
| description | Use when encountering errors or bugs to follow a systematic debugging process instead of jumping to conclusions. |
Debug Systematically
Follow a structured debugging process when encountering errors or unexpected behavior.
Debugging Process
1. Reproduce
Goal: Confirm the error consistently occurs
- Run the failing command/test again
- Document exact steps to reproduce
- Note any error messages verbatim
- Check if error is consistent or intermittent
Example:
make test
Error: undefined method 'foo' on line 42
2. Isolate
Goal: Narrow down to specific component/function
- Identify which component is failing
- Remove unrelated code to isolate issue
- Check if error occurs with minimal input
- Use binary search to find breaking change
Example:
go test ./pkg/auth/handler_test.go -v -run TestLogin
go test ./pkg/auth -v -run TestLogin/valid_credentials
3. Hypothesize
Goal: Form testable theories about the cause
- Based on error message, what could cause this?
- What changed recently that might affect this?
- What assumptions might be wrong?
- What edge cases aren't handled?
Examples:
- Hypothesis 1: Nil pointer - missing initialization
- Hypothesis 2: Type mismatch - wrong function signature
- Hypothesis 3: Race condition - concurrent access
4. Test Hypotheses
Goal: Verify each hypothesis systematically
- Test one hypothesis at a time
- Add logging/debugging to verify assumptions
- Check related code for similar patterns
- Look at test failures for clues
Example:
if handler.service == nil {
log.Printf("DEBUG: service is nil")
}
log.Printf("DEBUG: user type=%T, expected=*User", user)
5. Fix
Goal: Apply fix and verify it resolves the issue
- Implement the fix for confirmed root cause
- Run the originally failing test/command
- Verify fix doesn't break other functionality
- Clean up any debug logging
Example:
func NewHandler() *Handler {
return &Handler{
service: NewAuthService(),
}
}
6. Prevent
Goal: Add tests to prevent regression
- Add test case for the bug
- Add tests for related edge cases
- Document the fix if non-obvious
- Update error handling if needed
Example:
func TestHandlerWithNilService(t *testing.T) {
h := NewHandler()
require.NotNil(t, h.service)
}
When NOT to Skip Steps
Don't jump to conclusions:
- ❌ "This must be a nil pointer, let me add a nil check"
- ✅ "Let me reproduce the error and check what's actually nil"
Don't make multiple changes at once:
- ❌ Change 3 things, then test
- ✅ Change one thing, test, then proceed
Don't assume error messages are wrong:
- ❌ "That error doesn't make sense, ignore it"
- ✅ "That error is telling me exactly what's wrong"
Tools for Debugging
Logging
log.Printf("DEBUG: variable=%+v", variable)
Debugging Tools
dlv debug ./cmd/app
node --inspect-brk app.js
python -m pdb app.py
Test Isolation
go test -v -run TestSpecificFunction
npm test -- --testNamePattern="specific test"
pytest tests/test_file.py::test_function
Example Workflow
Problem: Test failing with "unexpected nil pointer"
Step 1 - Reproduce:
make test
Step 2 - Isolate:
go test ./pkg/auth -v -run TestLogin
Step 3 - Hypothesize:
- H1: handler.service not initialized
- H2: user.Session is nil
- H3: request.Context() returning nil
Step 4 - Test:
log.Printf("DEBUG: handler=%+v", handler)
Step 5 - Fix:
return &Handler{
service: NewAuthService(),
}
Step 6 - Prevent:
func TestNewHandler(t *testing.T) {
h := NewHandler()
require.NotNil(t, h.service)
}
Benefits
- Systematic approach prevents wasted effort
- Confirms root cause before fixing
- Prevents introducing new bugs
- Documents debugging process for future reference