Systematic debugging methodology — runtime errors, test failures, logic bugs, performance issues, production incidents. Five-step framework, root-cause analysis, browser/Node/Svelte tooling, and common bug patterns.
Scanned 5/27/2026
Install via CLI
openskills install JasonWarrenUK/goblin-mode---
name: debugging
description: Systematic debugging methodology — runtime errors, test failures, logic bugs, performance issues, production incidents. Five-step framework, root-cause analysis, browser/Node/Svelte tooling, and common bug patterns.
user-invocable: false
---
# Systematic Debugging
Methodical approach to identifying and fixing software issues. Emphasises reproducibility, isolation, and verification over trial-and-error debugging. Covers browser DevTools, Node debugging, logging strategies, and Svelte-specific debugging techniques.
---
## When This Skill Applies
Use this skill when:
- User reports something "not working"
- Investigating errors or unexpected behaviour
- Performance issues need diagnosis
- Setting up debugging infrastructure
- Explaining debugging approaches
- Teaching debugging techniques
- Questions about DevTools or debugging tools
---
## The Debugging Methodology
**Five-step process**: Reproduce → Isolate → Diagnose → Fix → Verify
This applies to every debugging scenario without exception.
### 1. Reproduce
**Make the bug happen reliably**
Can't fix what you can't reproduce. First priority is finding reliable steps to trigger the issue.
**Questions to ask**:
- What exact steps produce the error?
- Does it happen every time or intermittently?
- What's the expected vs actual behaviour?
- What's the minimal reproduction case?
**Document reproduction steps**:
````markdown
## To Reproduce
1. Navigate to `/dashboard`
2. Click "Load More" button
3. Scroll to bottom of page
4. Click "Load More" again
**Expected**: More items load
**Actual**: Page freezes, console shows error
**Frequency**: Happens every time on 2nd click
````
**If intermittent**:
- Look for race conditions
- Check network timing issues
- Consider state-dependent bugs
- Try multiple environments
### 2. Isolate (Root-Cause Depth)
**Narrow down the cause — then go deeper.**
Once reproducible, determine exactly where the problem originates. But don't stop at the first explanation. The first "cause" is often a symptom. Ask **why** repeatedly until you reach the structural root.
**The Five Whys**:
```
Bug: Users see stale data after updating their profile.
Why? → The cache isn't invalidated after the update.
Why? → The update function doesn't call cache.invalidate().
Why? → The caching layer was added after the update function was written.
Why? → There's no pattern ensuring new writes invalidate related caches.
Root: Missing cache invalidation convention. Fix the convention, not just this instance.
```
**Depth vs speed**: Not every bug warrants five whys. Use your judgement:
- **Shallow fix appropriate**: Typo, wrong variable name, missing null check
- **Deep investigation warranted**: Bug that could recur, affects multiple users, or reveals a pattern gap
**Isolation techniques**:
**Binary search approach**:
````typescript
// Working at line 50?
console.log('Check 1:', data); // ✓ Data good here
// Working at line 75?
console.log('Check 2:', result); // ✗ Result undefined here
// Problem is between lines 50-75
````
**Comment out code**:
````typescript
// Does removing this fix it?
// await someAsyncFunction();
// If yes, problem is in someAsyncFunction
````
**Minimal reproduction**:
````typescript
// Strip away everything non-essential
// Original: 300 lines, complex state, multiple API calls
// Minimal: 20 lines that show the exact issue
async function minimalRepro() {
const data = await fetch('/api/items');
console.log(data); // undefined when expected array
}
````
**Check assumptions**:
````typescript
// Assumption: API returns array
console.log(typeof data); // "object" - it's null!
// Assumption: User is logged in
console.log(user); // undefined - not logged in!
````
### 3. Diagnose
**Understand why it's happening**
- Runtime error: Type error, logic error, or environment issue?
- Test failure: Async issues, mock problems, environment differences, shared state?
- Logic bug: Off-by-one, wrong condition, state mutation, edge case?
- Performance: N+1 queries, large data, sync ops, memory leaks, inefficient algorithms?
- Production: Differences between local and production? Check logs, monitoring, external services.
### 4. Fix
**Implement targeted solution**
Now that you know the cause, fix it specifically.
**Fix patterns**:
**Null/undefined checks**:
````typescript
// Before (crashes)
const count = items.length;
// After
const count = items?.length ?? 0;
````
**Async timing**:
````typescript
// Before (race condition)
fetch('/api/data');
renderUI(); // Renders before data arrives
// After
const data = await fetch('/api/data');
renderUI(data);
````
**State initialisation**:
````typescript
// Before (undefined on first render)
let items;
// After
let items = [];
````
**Error boundaries**:
````typescript
// Before (crashes entire app)
const result = riskyOperation();
// After
try {
const result = riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
showErrorToUser('Something went wrong');
}
````
### 5. Verify
**Confirm the fix works**
Don't assume it's fixed. Test thoroughly.
**Verification checklist**:
- ✓ Original reproduction steps no longer produce error
- ✓ Expected behaviour now occurs
- ✓ No new errors introduced
- ✓ Edge cases still work
- ✓ Performance not degraded
**Test edge cases**:
````typescript
// Fixed for normal case, but what about:
- Empty array
- Null values
- Very large datasets
- Network failures
- Simultaneous requests
````
**Write regression test** (see Testing Foundations skill):
````typescript
it('should handle second "Load More" click', async () => {
render(ItemList);
await clickLoadMore();
await clickLoadMore(); // This used to crash
expect(screen.getAllByRole('listitem').length).toBeGreaterThan(10);
});
````
---
## Runtime Detection Pattern
Projects use different runtimes (npm, bun, deno). Always detect before running commands:
```bash
if [ -f "bun.lockb" ]; then
# bun project
elif [ -f "deno.json" ] || [ -f "deno.lock" ]; then
# deno project
else
# npm/pnpm/yarn project
fi
```
### Runtime-Specific Debug Commands
**Deno**:
```bash
deno test --watch path/to/test.ts
deno run --inspect-brk script.ts # Debug with Chrome DevTools
```
**Bun**:
```bash
bun test --watch path/to/test.ts
bun --inspect script.ts # Debug
bun --hot script.ts # Hot reload
```
**npm**:
```bash
npm test -- --watch path/to/test.ts
node --inspect-brk script.js # Debug
```
---
## Browser DevTools
### Console
**Strategic logging**:
````typescript
// ✗ Bad: Non-informative
console.log(data);
// ✓ Good: Labelled and contextual
console.log('API Response:', data);
console.log('User state before update:', user);
// ✓ Better: Grouped related logs
console.group('Data Processing');
console.log('Input:', rawData);
console.log('Processed:', processedData);
console.log('Output:', finalResult);
console.groupEnd();
// ✓ Advanced: Conditional logging
const DEBUG = true;
DEBUG && console.log('Debug info:', state);
// ✓ Tables for arrays of objects
console.table(users);
````
**Console methods**:
````typescript
console.log('Normal message');
console.info('Informational');
console.warn('Warning - check this');
console.error('Error occurred');
console.debug('Debug details');
// Timing
console.time('operation');
// ... code to measure
console.timeEnd('operation'); // operation: 45.2ms
// Assertions
console.assert(value > 0, 'Value must be positive:', value);
// Count occurrences
console.count('API call'); // API call: 1
console.count('API call'); // API call: 2
````
### Network Tab
**Inspect API requests**:
- Check request URL and method
- Verify headers (Authorization, Content-Type)
- Inspect request payload
- Check response status and body
- Look for failed requests (red)
- Check timing (slow responses)
**Common issues**:
````
404 Not Found → Check URL spelling, route exists
401 Unauthorized → Check auth token present and valid
500 Server Error → Check server logs, API issue
CORS Error → Check server allows origin
Timeout → API too slow or not responding
````
### Elements Tab
**Inspect DOM**:
- Check element exists
- Verify classes applied
- Check computed styles
- Inspect event listeners
- Modify styles live
**Common CSS issues**:
````css
/* Check computed styles for: */
- Display: none (hidden element)
- Z-index conflicts
- Overflow: hidden (content clipped)
- Position issues
- Flexbox/Grid properties
````
### Sources Tab (Debugger)
**Breakpoints**:
````typescript
function processData(data) {
// Set breakpoint on this line
const result = transform(data);
// Execution pauses, inspect:
// - data value
// - Scope variables
// - Call stack
return result;
}
````
**Breakpoint types**:
- **Line breakpoints** - Pause at specific line
- **Conditional breakpoints** - Pause only when condition true
- **Logpoints** - Log without stopping execution
- **Exception breakpoints** - Pause on errors
**Debugger controls**:
- **Continue** (F8) - Resume execution
- **Step over** (F10) - Execute current line, don't go into functions
- **Step into** (F11) - Go into function calls
- **Step out** (Shift+F11) - Exit current function
### Application Tab
**Inspect storage**:
- **Local Storage** - Persistent key-value storage
- **Session Storage** - Session-only storage
- **Cookies** - Check authentication cookies
- **IndexedDB** - Client-side database
- **Cache Storage** - Service worker cache
**Common storage issues**:
````typescript
// Check if data stored correctly
localStorage.getItem('user'); // "undefined" as string? null?
// Check cookie domain/path
// Check expiration
// Check HttpOnly/Secure flags
````
### Performance Tab
**Profile performance**:
1. Click record
2. Perform slow action
3. Stop recording
4. Analyse flame chart
**Look for**:
- Long tasks (>50ms)
- Excessive re-renders
- Slow API calls
- Memory leaks (increasing usage)
---
## Node.js Debugging
### Built-in Debugger
**Start with inspect**:
````bash
node --inspect server.js
# or
node --inspect-brk server.js # Break on first line
````
**Chrome DevTools**:
1. Open chrome://inspect
2. Click "Open dedicated DevTools for Node"
3. Same interface as browser debugging
### Console Debugging
````typescript
// Strategic placement
async function fetchUserData(userId) {
console.log('Fetching user:', userId);
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
console.log('Query result:', user);
if (!user) {
console.log('User not found');
return null;
}
console.log('Returning user:', user);
return user;
}
````
### Debug Module
````typescript
import debug from 'debug';
const log = debug('app:users');
log('Fetching user %s', userId); // Only shows if DEBUG=app:* enabled
// Enable specific namespaces
// DEBUG=app:users node server.js
// DEBUG=app:* node server.js
// DEBUG=* node server.js
````
---
## Svelte-Specific Debugging
### Reactive Statements
**Log reactive changes**:
````svelte
<script>
let count = 0;
// Debug reactive statement
$: console.log('Count changed:', count);
// Debug derived value
$: doubled = count * 2;
$: console.log('Doubled:', doubled);
// Debug with condition
$: if (count > 10) {
console.log('Count exceeded threshold');
}
</script>
````
### Component Lifecycle
````svelte
<script>
import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte';
onMount(() => {
console.log('Component mounted');
return () => console.log('Component cleanup');
});
onDestroy(() => {
console.log('Component destroyed');
});
beforeUpdate(() => {
console.log('Before DOM update');
});
afterUpdate(() => {
console.log('After DOM update');
});
</script>
````
### Store Debugging
````typescript
import { writable } from 'svelte/store';
function createDebugStore(name, initial) {
const store = writable(initial);
const { subscribe, set, update } = store;
return {
subscribe,
set: (value) => {
console.log(`${name} set to:`, value);
set(value);
},
update: (fn) => {
console.log(`${name} updated`);
update((val) => {
const newVal = fn(val);
console.log(` Old:`, val, `New:`, newVal);
return newVal;
});
}
};
}
export const userStore = createDebugStore('user', null);
````
### Svelte DevTools
**Browser extension** - Install Svelte DevTools
Features:
- Component tree inspection
- Props and state values
- Store contents
- Event listeners
- Performance profiling
---
## Logging Strategies
### Log Levels
````typescript
const LOG_LEVELS = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3
};
const currentLevel = LOG_LEVELS.INFO;
function log(level, message, ...args) {
if (level <= currentLevel) {
const prefix = ['ERROR', 'WARN', 'INFO', 'DEBUG'][level];
console.log(`[${prefix}]`, message, ...args);
}
}
// Usage
log(LOG_LEVELS.ERROR, 'Failed to load user');
log(LOG_LEVELS.DEBUG, 'Cache hit for key:', key); // Only shows if DEBUG level
````
### Contextual Logging
````typescript
function createLogger(context) {
return {
info: (msg, ...args) => console.log(`[${context}]`, msg, ...args),
error: (msg, ...args) => console.error(`[${context}]`, msg, ...args),
debug: (msg, ...args) => console.debug(`[${context}]`, msg, ...args)
};
}
const log = createLogger('UserService');
log.info('Fetching user', userId);
log.error('Failed to fetch', error);
````
### Production Logging
**Don't ship debug logs**:
````typescript
// ✗ Bad: Logs in production
console.log('User clicked button');
// ✓ Good: Conditional logging
if (import.meta.env.DEV) {
console.log('User clicked button');
}
// ✓ Better: Logging service
logger.info('User action', { action: 'button_click', userId });
````
---
## Common Bug Patterns
### Race Conditions
**Problem**:
````typescript
// Race: Which finishes first?
fetchUserData(userId);
fetchUserPosts(userId);
render(); // Might render before data arrives
````
**Solution**:
````typescript
const [userData, userPosts] = await Promise.all([
fetchUserData(userId),
fetchUserPosts(userId)
]);
render(userData, userPosts);
````
### Stale Closures
**Problem**:
````svelte
<script>
let count = 0;
function startTimer() {
setInterval(() => {
console.log(count); // Always logs 0 (stale closure)
count++;
}, 1000);
}
</script>
````
**Solution**:
````svelte
<script>
let count = 0;
function startTimer() {
setInterval(() => {
count = count + 1; // Access current value via update
}, 1000);
}
</script>
````
### Undefined Reference Errors
**Problem**:
````typescript
const name = user.profile.name; // Cannot read property 'name' of undefined
````
**Solution**:
````typescript
// Optional chaining
const name = user?.profile?.name;
// With default
const name = user?.profile?.name ?? 'Unknown';
````
### Memory Leaks
**Problem**:
````svelte
<script>
import { onMount } from 'svelte';
onMount(() => {
const interval = setInterval(() => {
// Do work
}, 1000);
// Never cleaned up! Memory leak
});
</script>
````
**Solution**:
````svelte
<script>
import { onMount } from 'svelte';
onMount(() => {
const interval = setInterval(() => {
// Do work
}, 1000);
return () => clearInterval(interval); // Cleanup
});
</script>
````
---
## Performance Debugging
### Identify Slow Operations
````typescript
function measurePerformance(label, fn) {
const start = performance.now();
const result = fn();
const end = performance.now();
console.log(`${label}: ${(end - start).toFixed(2)}ms`);
return result;
}
````
### Find Memory Leaks
**Chrome DevTools Memory Tab**:
1. Take heap snapshot
2. Perform action
3. Take another snapshot
4. Compare snapshots
5. Look for growing objects
---
## TypeScript Debugging
### Type Errors as Debugging Aid
````typescript
// TypeScript catches bugs at compile time
function getUser(id: string): User {
return fetchUser(id); // Error: fetchUser expects number
// Bug found before runtime!
}
````
### Type Narrowing
````typescript
function processValue(value: string | number) {
if (typeof value === 'string') {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
````
---
## Root-Cause vs Symptom Fixes
**Symptom fix**: Fixes the immediate problem but doesn't prevent recurrence.
**Root-cause fix**: Addresses the structural issue that allowed the bug to exist.
```typescript
// Symptom fix: Add null check where the crash happens
const name = user?.profile?.name ?? 'Unknown';
// Root-cause fix: Ensure profile is always populated at creation
async function createUser(data: CreateUserRequest): Promise<User> {
return await db.users.create({
...data,
profile: { name: data.name } // Profile guaranteed at creation
});
}
```
**When to ship a symptom fix**: When the root cause is expensive to fix and the symptom fix is safe. But always log the root cause as a follow-up task.
**When to insist on root-cause fix**: When the bug pattern could recur in other places, when data integrity is at risk, or when the symptom fix introduces its own complexity.
---
## Debugging Checklist
**Before:**
- [ ] Can you reproduce it?
- [ ] Captured error/stack trace?
- [ ] Know when it started?
**During:**
- [ ] Isolated problem area?
- [ ] Checked assumptions?
- [ ] Reviewed recent changes?
- [ ] Documenting attempts?
**After:**
- [ ] Root cause fixed?
- [ ] Test added?
- [ ] Nothing else broken?
- [ ] Solution documented?
No comments yet. Be the first to comment!