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.

install
source · Clone the upstream repo
git clone https://github.com/JasonWarrenUK/goblin-mode
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/debugging/SKILL.md
source content

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:

## 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:

  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:

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

// 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:

  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 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?