$npx -y skills add sabahattink/antigravity-fullstack-hq --skill systematic-debuggingDebugging methodology, hypothesis testing, reading stack traces, isolating issues. Use when facing an unexpected bug, a flaky test, a production incident, or any situation where the cause isn't immediately obvious.
| 1 | # Systematic Debugging |
| 2 | |
| 3 | ## The Scientific Method Applied to Bugs |
| 4 | |
| 5 | ``` |
| 6 | 1. OBSERVE — Reproduce the issue reliably |
| 7 | 2. HYPOTHESIZE — Form the simplest explanation consistent with symptoms |
| 8 | 3. PREDICT — "If my hypothesis is correct, then X should be true" |
| 9 | 4. TEST — Run an experiment to falsify the hypothesis |
| 10 | 5. CONCLUDE — If wrong, refine hypothesis and repeat |
| 11 | ``` |
| 12 | |
| 13 | Never jump to a fix before you understand the cause. A fix without understanding is guessing. |
| 14 | |
| 15 | ## Step 1: Reproduce Reliably |
| 16 | |
| 17 | You cannot debug a bug you cannot reproduce. |
| 18 | |
| 19 | ```bash |
| 20 | # Note the exact conditions: |
| 21 | # - Input / request body |
| 22 | # - Environment (local / staging / prod) |
| 23 | # - User account or data state |
| 24 | # - Frequency (always / sometimes / once) |
| 25 | # - When it started (after which deploy?) |
| 26 | |
| 27 | # Find the last good commit |
| 28 | git bisect start |
| 29 | git bisect bad HEAD |
| 30 | git bisect good v1.2.3 # last known good tag |
| 31 | # git bisect then checks out midpoints — test and mark good/bad |
| 32 | ``` |
| 33 | |
| 34 | ## Step 2: Read the Stack Trace |
| 35 | |
| 36 | ``` |
| 37 | Error: Cannot read properties of undefined (reading 'email') |
| 38 | at UserService.findOneOrFail (/src/users/users.service.ts:42:23) |
| 39 | at UsersController.findOne (/src/users/users.controller.ts:28:31) |
| 40 | at ... |
| 41 | |
| 42 | Reading strategy: |
| 43 | 1. Top line: the actual error — read it carefully word by word |
| 44 | 2. First frame after your code: where it crashed (users.service.ts:42) |
| 45 | 3. Frame above that: what called it (users.controller.ts:28) |
| 46 | 4. Ignore node_modules frames unless diagnosing a library issue |
| 47 | ``` |
| 48 | |
| 49 | ```typescript |
| 50 | // users.service.ts line 42 — the crash site |
| 51 | async findOneOrFail(id: number): Promise<User> { |
| 52 | const user = await this.repo.findById(id) |
| 53 | // line 42: user is undefined, not null — we expected null |
| 54 | return user // accessing .email somewhere downstream fails |
| 55 | } |
| 56 | |
| 57 | // Fix: repo.findOne returns undefined when not found, but our types say null |
| 58 | // The contract mismatch is the root cause, not the downstream access |
| 59 | ``` |
| 60 | |
| 61 | ## Step 3: Isolate the Problem |
| 62 | |
| 63 | Binary search through the call stack to find where the invariant breaks. |
| 64 | |
| 65 | ```typescript |
| 66 | // Add strategic logging — not everywhere, but at the boundary |
| 67 | async findOneOrFail(id: number): Promise<User> { |
| 68 | console.log('[DEBUG] findOneOrFail called with', { id, type: typeof id }) |
| 69 | const user = await this.repo.findById(id) |
| 70 | console.log('[DEBUG] repo returned', { user, type: user === null ? 'null' : typeof user }) |
| 71 | // ... |
| 72 | } |
| 73 | |
| 74 | // Now you know: is `id` the wrong value, or does the repo return unexpected type? |
| 75 | ``` |
| 76 | |
| 77 | ```bash |
| 78 | # Isolate environment issues |
| 79 | NODE_ENV=production node -e "require('./dist/main')" # test prod build locally |
| 80 | |
| 81 | # Isolate database issues — run the query directly |
| 82 | psql $DATABASE_URL -c "SELECT * FROM users WHERE id = 42;" |
| 83 | |
| 84 | # Isolate network issues |
| 85 | curl -v -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/users/42 |
| 86 | ``` |
| 87 | |
| 88 | ## Common Bug Patterns |
| 89 | |
| 90 | ### Async/Await Mistakes |
| 91 | ```typescript |
| 92 | // BUG: missing await — returns Promise, not value |
| 93 | async function getBadge(userId: string) { |
| 94 | const user = this.repo.findById(userId) // ← missing await |
| 95 | return user.role === 'admin' ? 'admin' : 'user' // TypeError: user.role undefined |
| 96 | } |
| 97 | |
| 98 | // BUG: forEach with async — fires and forgets |
| 99 | async function notifyAll(userIds: string[]) { |
| 100 | userIds.forEach(async id => { // ← async inside forEach is a trap |
| 101 | await this.emailService.send(id) |
| 102 | }) |
| 103 | // returns before any emails sent! |
| 104 | } |
| 105 | |
| 106 | // FIX: use Promise.all or for...of |
| 107 | async function notifyAll(userIds: string[]) { |
| 108 | await Promise.all(userIds.map(id => this.emailService.send(id))) |
| 109 | // OR (sequential): |
| 110 | for (const id of userIds) { |
| 111 | await this.emailService.send(id) |
| 112 | } |
| 113 | } |
| 114 | ``` |
| 115 | |
| 116 | ### Stale Closure |
| 117 | ```typescript |
| 118 | // BUG: stale closure captures old value |
| 119 | function Timer() { |
| 120 | const [count, setCount] = useState(0) |
| 121 | |
| 122 | useEffect(() => { |
| 123 | const id = setInterval(() => { |
| 124 | setCount(count + 1) // ← count is always 0 in this closure |
| 125 | }, 1000) |
| 126 | return () => clearInterval(id) |
| 127 | }, []) // ← empty deps — count never updates in closure |
| 128 | |
| 129 | // FIX: use functional update |
| 130 | useEffect(() => { |
| 131 | const id = setInterval(() => { |
| 132 | setCount(c => c + 1) // ← always uses latest value |
| 133 | }, 1000) |
| 134 | return () => clearInterval(id) |
| 135 | }, []) |
| 136 | } |
| 137 | ``` |
| 138 | |
| 139 | ### Race Condition |
| 140 | ```typescript |
| 141 | // BUG: two concurrent requests overwrite each other |
| 142 | async function incrementViews(postId: string) { |
| 143 | const post = await db.posts.findOne(postId) |
| 144 | post.views++ |
| 145 | await db.posts.save(post) // request B may have read same value |
| 146 | } |
| 147 | |
| 148 | // FIX: atomic update |
| 149 | async function incrementViews(postId: string) { |
| 150 | await db.posts.increment({ id: postId }, 'views', 1) |
| 151 | // or: UPDATE posts SET views = views + 1 WHERE id = $1 |
| 152 | } |
| 153 | ``` |
| 154 | |
| 155 | ### TypeScript Lies |
| 156 | ```typescript |
| 157 | // Type assertions hide runtime issues |
| 158 | const user = result as User // if result is nul |