Goblin-mode debugging
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.
git clone https://github.com/JasonWarrenUK/goblin-mode
T=$(mktemp -d) && git clone --depth=1 https://github.com/JasonWarrenUK/goblin-mode "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/debugging" ~/.claude/skills/jasonwarrenuk-goblin-mode-debugging && rm -rf "$T"
skills/debugging/SKILL.mdSystematic 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:
## 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:
// 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:
// Does removing this fix it? // await someAsyncFunction(); // If yes, problem is in someAsyncFunction
Minimal reproduction:
// 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:
// 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:
// Before (crashes) const count = items.length; // After const count = items?.length ?? 0;
Async timing:
// Before (race condition) fetch('/api/data'); renderUI(); // Renders before data arrives // After const data = await fetch('/api/data'); renderUI(data);
State initialisation:
// Before (undefined on first render) let items; // After let items = [];
Error boundaries:
// 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:
// 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):
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:
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:
deno test --watch path/to/test.ts deno run --inspect-brk script.ts # Debug with Chrome DevTools
Bun:
bun test --watch path/to/test.ts bun --inspect script.ts # Debug bun --hot script.ts # Hot reload
npm:
npm test -- --watch path/to/test.ts node --inspect-brk script.js # Debug
Browser DevTools
Console
Strategic logging:
// ✗ 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:
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:
/* Check computed styles for: */ - Display: none (hidden element) - Z-index conflicts - Overflow: hidden (content clipped) - Position issues - Flexbox/Grid properties
Sources Tab (Debugger)
Breakpoints:
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:
// 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:
- Click record
- Perform slow action
- Stop recording
- 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:
node --inspect server.js # or node --inspect-brk server.js # Break on first line
Chrome DevTools:
- Open chrome://inspect
- Click "Open dedicated DevTools for Node"
- Same interface as browser debugging
Console Debugging
// 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
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:
<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
<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
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
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
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:
// ✗ 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:
// Race: Which finishes first? fetchUserData(userId); fetchUserPosts(userId); render(); // Might render before data arrives
Solution:
const [userData, userPosts] = await Promise.all([ fetchUserData(userId), fetchUserPosts(userId) ]); render(userData, userPosts);
Stale Closures
Problem:
<script> let count = 0; function startTimer() { setInterval(() => { console.log(count); // Always logs 0 (stale closure) count++; }, 1000); } </script>
Solution:
<script> let count = 0; function startTimer() { setInterval(() => { count = count + 1; // Access current value via update }, 1000); } </script>
Undefined Reference Errors
Problem:
const name = user.profile.name; // Cannot read property 'name' of undefined
Solution:
// Optional chaining const name = user?.profile?.name; // With default const name = user?.profile?.name ?? 'Unknown';
Memory Leaks
Problem:
<script> import { onMount } from 'svelte'; onMount(() => { const interval = setInterval(() => { // Do work }, 1000); // Never cleaned up! Memory leak }); </script>
Solution:
<script> import { onMount } from 'svelte'; onMount(() => { const interval = setInterval(() => { // Do work }, 1000); return () => clearInterval(interval); // Cleanup }); </script>
Performance Debugging
Identify Slow Operations
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:
- Take heap snapshot
- Perform action
- Take another snapshot
- Compare snapshots
- Look for growing objects
TypeScript Debugging
Type Errors as Debugging Aid
// TypeScript catches bugs at compile time function getUser(id: string): User { return fetchUser(id); // Error: fetchUser expects number // Bug found before runtime! }
Type Narrowing
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.
// 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?