Debugging Guide | Common Errors, Root Cause Analysis & Debugging Strategies

Debugging Guide | Common Errors, Root Cause Analysis & Debugging Strategies

이 글의 핵심

Debugging is the skill that separates good developers from great ones. This guide covers systematic debugging strategies, reading stack traces, common error patterns in JavaScript and Python, browser DevTools, Node.js debugging, and production incident response.

The Debugging Mindset

Bugs have causes. Finding the cause — not patching the symptom — is the goal.

Bad debugging:           Good debugging:
  See error              Read error message carefully
  Try random fix         Form hypothesis about cause
  Still broken           Test hypothesis (add log, isolate)
  Try another fix        Confirm root cause
  45 mins later: ???     Fix the root cause
                         Understand why it happened

The process:

  1. Read the error message (the full message, not just the last line)
  2. Find where the error occurs (file, line number)
  3. Form a hypothesis about the cause
  4. Test the hypothesis (don’t guess-fix)
  5. Fix the root cause (not the symptom)
  6. Verify the fix doesn’t break other things

Reading Stack Traces

Stack traces show the call chain when an error occurred — read from top (where it broke) to bottom (where it started):

// JavaScript stack trace
TypeError: Cannot read properties of undefined (reading 'name')
    at getUserName (utils.js:15:20)    ← where it broke
    at renderProfile (Profile.jsx:42:5)
    at renderUser (App.jsx:78:3)       ← where execution started
Error type:  TypeError (reading a property of undefined)
Message:     Cannot read properties of undefined (reading 'name')
Location:    utils.js line 15, column 20
# Python stack trace — read from bottom to top
Traceback (most recent call last):
  File "app.py", line 45, in get_user    ← started here
    return process_user(db.find(user_id))
  File "utils.py", line 12, in process_user
    return user['name'].upper()           ← broke here
KeyError: 'name'

JavaScript / TypeScript Common Errors

TypeError: Cannot read properties of undefined

// Error: user is undefined when you access user.name
const name = user.name;  // TypeError if user = undefined

// Causes:
// 1. Async data not yet loaded
// 2. API returned null/undefined
// 3. Object key typo

// Fix strategies:
// 1. Optional chaining
const name = user?.name ?? 'Unknown';

// 2. Guard clause
if (!user) return null;
const name = user.name;

// 3. Check async loading
if (isLoading) return <Spinner />;
const name = user.name;  // Safe now

Async/Await Mistakes

// ❌ Missing await — promise returned, not resolved value
async function getUser(id) {
  const user = fetch(`/api/users/${id}`);  // Missing await!
  console.log(user);  // Logs: Promise { <pending> }
  return user.name;   // undefined
}

// ✅ Correct
async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();  // Also await the json()
  return user.name;
}

// ❌ Sequential when parallel is possible
const user = await getUser(id);
const posts = await getPosts(id);  // Waits for getUser unnecessarily

// ✅ Parallel
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);

React-Specific Errors

// Error: "Cannot update a component while rendering a different component"
// Cause: setState called during render
function BadComponent({ data }) {
  const [count, setCount] = useState(0);
  setCount(data.length);  // ❌ setState during render

  return <div>{count}</div>;
}

// Fix: use useEffect or calculate directly
function GoodComponent({ data }) {
  const count = data.length;  // ✅ calculate, don't set state
  return <div>{count}</div>;
}
// Error: "Each child in a list should have a unique key prop"
// Fix: add unique key to list items
items.map(item => (
  <ListItem key={item.id} data={item} />  // key must be unique and stable
));

// ❌ Don't use index as key if list reorders/filters
items.map((item, index) => <Item key={index} />);
// ✅ Use stable ID
items.map(item => <Item key={item.id} />);

Closure / Stale State

// ❌ Stale closure — count is always 0 in the callback
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count);  // Always 0 — captured at effect creation
      setCount(count + 1); // Bug: always sets to 1
    }, 1000);
    return () => clearInterval(timer);
  }, []);  // No deps — effect never re-runs

// ✅ Use functional update
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1);  // c is always current value
    }, 1000);
    return () => clearInterval(timer);
  }, []);
}

Python Common Errors

# IndentationError — mixing tabs and spaces
def greet(name):
    print(f"Hello, {name}")  # spaces
	return name              # tab — IndentationError!

# Fix: use consistent spaces (4 spaces standard) or configure editor

# NameError — variable used before definition or wrong scope
def calculate():
    result = total * 2  # NameError: total not defined
    total = 100         # defined after use

# AttributeError — wrong method name or None returned
user = get_user(id)     # returns None if not found
user.name               # AttributeError: 'NoneType' has no attribute 'name'

# Fix:
user = get_user(id)
if user is None:
    raise ValueError(f"User {id} not found")
user.name  # Safe

# KeyError — dictionary key doesn't exist
data = {'name': 'Alice'}
age = data['age']   # KeyError: 'age'

# Fix:
age = data.get('age', 0)        # Default value
age = data.get('age')           # Returns None if missing
if 'age' in data: age = data['age']

# TypeError — wrong argument types
def add(a: int, b: int) -> int:
    return a + b

add("1", 2)  # TypeError: can only concatenate str (not "int") to str

# Fix: type check or convert
add(int("1"), 2)

Browser DevTools

Console

// Beyond console.log — use these:
console.table(arrayOfObjects);   // Tabular view of objects
console.group('API Call');       // Collapsible group
console.log('request:', req);
console.groupEnd();
console.time('db query');        // Time measurement
await db.query(sql);
console.timeEnd('db query');     // → "db query: 45.23ms"
console.trace();                 // Print stack trace at current point
console.dir(element, { depth: 3 }); // Deep inspection of object

Network Tab

What to check when API calls fail:
  Request URL:   is it the right endpoint?
  Request method: GET/POST/etc correct?
  Request headers: Authorization header present?
  Request payload: is the body correct JSON?
  Response status: 200/401/404/500?
  Response body: what did the server actually return?

Filter: XHR/Fetch to see only API calls
Preserve log: keeps logs across page navigations

Sources / Debugger

// Set breakpoints in code
debugger;  // Browser pauses here — inspect variables, step through

// Or: click line number in Sources panel

// Step controls:
// F10 = Step over (execute next line, don't enter functions)
// F11 = Step into (enter the function on this line)
// F12 = Step out (complete current function, return to caller)
// F8  = Continue (run until next breakpoint)

Performance Tab

Record performance:
  1. Open DevTools → Performance
  2. Click Record
  3. Interact with the page
  4. Stop recording

Look for:
  Long Tasks (red) — JS blocking main thread > 50ms
  Layout thrashing — forced reflows in a loop
  Large paint events — slow rendering
  Memory leaks — heap size growing over time

Node.js Debugging

VS Code Debugger

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Node.js",
      "program": "${workspaceFolder}/src/index.ts",
      "runtimeExecutable": "ts-node",
      "env": { "NODE_ENV": "development" }
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to running process",
      "port": 9229
    }
  ]
}
# Start Node.js with debugger
node --inspect src/index.js      # Debugger on port 9229
node --inspect-brk src/index.js  # Break on first line

# Open chrome://inspect in Chrome → click "inspect"

Structured Logging

// Replace ad-hoc console.log with structured logs
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: process.env.NODE_ENV === 'development'
    ? { target: 'pino-pretty' }
    : undefined,
});

// Include context, not just messages
logger.info({ userId: 1, action: 'login', ip: req.ip }, 'User logged in');
logger.error({ err, userId: 1 }, 'Login failed');

// Filter in production:
// LOG_LEVEL=debug to see debug logs
// LOG_LEVEL=error to see only errors

Performance Debugging

Finding Slow Functions

// Measure execution time
console.time('expensive function');
expensiveFunction();
console.timeEnd('expensive function');

// Profile with performance API
const start = performance.now();
result = processData(largeArray);
const duration = performance.now() - start;
console.log(`processData: ${duration.toFixed(2)}ms`);

// Node.js: use --prof flag for V8 profiler
node --prof src/index.js
# Creates isolate-*.log
node --prof-process isolate-*.log > processed.txt
# Shows which functions consume most time

Memory Leaks

// Common leak: event listeners not removed
function BadComponent() {
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    // Missing cleanup!
  }, []);

  return <div />;
}

// Fix: cleanup in useEffect return
function GoodComponent() {
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);  // Cleanup
  }, []);

  return <div />;
}

// Detect in Node.js:
node --expose-gc src/index.js
// Then: global.gc() to force GC, check heapUsed before/after

Production Debugging Checklist

# 1. Check error logs first
kubectl logs my-pod --previous    # Kubernetes
journalctl -u my-service -n 100  # systemd
tail -f /var/log/app.log | grep ERROR

# 2. Check recent deployments
git log --oneline -10             # Recent commits
git diff HEAD~1                   # What changed?

# 3. Check system resources
top / htop                        # CPU/memory
df -h                             # Disk space
netstat -tlnp | grep :8000       # Port listening?

# 4. Test the specific failing endpoint
curl -v https://api.example.com/users/1
# Check: status code, response body, headers, timing

# 5. Reproduce locally with production data/config
DATABASE_URL=prod_url node src/index.js
# Never run mutations against production data for debugging!

Rubber Duck Debugging

When stuck, explain the problem out loud (or in writing):

"The user endpoint returns 404 when the user exists in the database.
I know the database has the user because I queried it directly.
The route is registered — I can see it in the route list.
Wait — I just said it: the route IS registered. Let me check the
middleware order... oh. The auth middleware is rejecting before
the route handler runs. The token is expired."

The act of explaining forces you to state your assumptions explicitly — and usually one of those assumptions is wrong.

Related posts: