HomeProjectsBlogContact
Download CV
Back to Blog

Debugging Like a Detective: The Art of Finding Bugs Before They Find You

"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian Kernighan

Debugging Like a Detective: The Art of Finding Bugs Before They Find You

You’ve been staring at the same function for 45 minutes. The test was passing yesterday. Nothing changed. Or at least, you think nothing changed. You add a console.log. Then another. Then twelve more. You comment out half the file. You restart the server. You restart your computer. You restart your career expectations.

Sound familiar?

Debugging is the part of software engineering nobody brags about at parties — but it’s quietly the skill that separates good developers from great ones. The best engineers aren’t the ones who write bug-free code (that person doesn’t exist). They’re the ones who find bugs fast, understand them deeply, and fix them correctly the first time.

This is your guide to becoming one of those people.


Table of Contents

  1. The Psychology of Debugging
  2. The Scientific Method for Code
  3. The Bug Taxonomy: Know Your Enemy
  4. Rubber Duck Debugging & Other Weird Techniques That Work
  5. Reading Stack Traces Like a Pro
  6. The Art of the Minimal Reproduction
  7. Debugger Tools You Should Actually Be Using
  8. printf Debugging Done Right
  9. Debugging in Production (Without Panicking)
  10. The Top 10 Bugs That Haunt Every Developer
  11. Preventing Bugs Before They’re Born
  12. When to Ask for Help (And How to Do It Well)
  13. The Debugging Mindset: A Philosophy

1. The Psychology of Debugging

Before we talk tools and techniques, let’s talk about your brain — because it’s simultaneously your greatest debugging asset and your worst enemy.

The Curse of Confidence

The number one debugging mistake developers make is assuming they know where the bug is before they’ve confirmed it.

Your brain is a prediction machine. When you read code you wrote, it autocompletes what should be there rather than what is there. This is why you can read your own typo five times and not see it — but a colleague spots it in three seconds.

The Debugging Confidence Trap:

You think the bug is in Module A
    → You look at Module A
    → Module A looks fine (because you wrote it confidently)
    → You look at Module A again, harder
    → You refactor Module A
    → Bug still there
    → You blame JavaScript / the database / Mercury retrograde
    → Bug was in Module B all along

The fix: Treat every assumption as a hypothesis to be tested, not a fact.

Emotional Debugging (The Bad Kind)

There’s a specific kind of cognitive impairment that happens when you’re frustrated and debugging. Your thinking narrows. You try the same thing four times expecting different results. You make changes without understanding them.

Recognize these signs that you need a break:

  • You’ve been on the same bug for more than 2 hours without progress
  • You’re making changes and hoping (not reasoning) they’ll work
  • You’ve opened Stack Overflow for the 6th time and you’re reading the same answers
  • You’re considering rewriting the entire module from scratch

The fix is embarrassingly simple: Walk away for 20 minutes. Get coffee. The bug will still be there when you come back, but your prefrontal cortex will be firing again.

The “It Works on My Machine” Phenomenon

┌─────────────────────────────────────────────────────────────┐
│         DEVELOPER'S MENTAL MODEL vs REALITY                 │
│                                                             │
│  Developer thinks:           What's actually happening:     │
│                                                             │
│  My code                     My code                       │
│  ───────────                 ───────────                   │
│  Input → Output              Input                         │
│                                  ↓                         │
│                              OS timezone                   │
│                                  ↓                         │
│                              Locale settings               │
│                                  ↓                         │
│                              Node version                  │
│                                  ↓                         │
│                              Environment vars              │
│                                  ↓                         │
│                              Cached state                  │
│                                  ↓                         │
│                              Output                        │
└─────────────────────────────────────────────────────────────┘

When your code works locally but breaks in CI or production, the bug isn’t in your logic — it’s in the environment. Always narrow down what’s different between the two environments.


2. The Scientific Method for Code

The best debuggers don’t guess randomly. They apply a structured method. And it turns out the scientific method — the same one from high school chemistry — maps perfectly to debugging.

┌─────────────────────────────────────────────────────────────┐
│           THE DEBUGGING SCIENTIFIC METHOD                   │
│                                                             │
│  1. OBSERVE                                                 │
│     "The checkout button doesn't work on Firefox mobile"   │
│     └── Be specific. Vague observations lead nowhere.      │
│                                                             │
│  2. HYPOTHESIZE                                             │
│     "Probably a CSS issue with the z-index on the button"  │
│     └── Generate 2-3 candidates before investigating any.  │
│                                                             │
│  3. PREDICT                                                 │
│     "If it's z-index, the button exists in the DOM but     │
│      is being covered by another element"                  │
│     └── What would you expect to see if the hypo is true?  │
│                                                             │
│  4. TEST                                                    │
│     Inspect element on Firefox mobile → check computed CSS  │
│     └── One change at a time. Never two simultaneously.    │
│                                                             │
│  5. CONCLUDE                                                │
│     "It was z-index — the modal overlay had z-index: 9999  │
│      and wasn't being dismissed properly on touch"          │
│     └── Understand WHY, not just what fixed it.            │
│                                                             │
│  6. DOCUMENT                                                │
│     Add a comment. Open a ticket. Update the runbook.      │
│     └── The next person (probably you) will thank you.    │
└─────────────────────────────────────────────────────────────┘

The Hypothesis Ranking Trick

When you have multiple possible causes, rank them before testing:

Bug: "User's cart empties randomly"

Hypotheses (ranked by probability):
├── 1. Session timeout too short → Most likely (it's "random" = time-based)
├── 2. Cart state not persisted between browser tabs → Plausible
├── 3. Race condition in cart update API → Possible
├── 4. Browser's private mode clearing storage → Unlikely but testable
└── 5. Cosmic rays flipping bits in RAM → Least likely (but never say never)

→ Test #1 first. Check session TTL in config.

This keeps you from spending 3 hours on hypothesis #5 before checking #1.


3. The Bug Taxonomy: Know Your Enemy

Not all bugs are created equal. Understanding what kind of bug you’re dealing with tells you how to find it.

The Eight Species of Bug

┌──────────────────┬─────────────────────────────┬─────────────────────────┐
│  Bug Type        │  Description                │  Typical Habitat        │
├──────────────────┼─────────────────────────────┼─────────────────────────┤
│  Typo/Syntax     │  Code says wrong thing      │  Anywhere, obvious      │
│  Off-by-One      │  Loop goes one too far/near │  Arrays, pagination     │
│  Race Condition  │  Timing-dependent failure   │  Async code, threads    │
│  Memory Leak     │  Resources never freed      │  Long-running processes │
│  State Bug       │  Wrong value at wrong time  │  Complex UIs, stateful  │
│  Integration Bug │  Works alone, breaks together│ APIs, microservices    │
│  Environment Bug │  Works here, fails there   │  Config, paths, env vars│
│  Heisenbug       │  Disappears when observed   │  Concurrency, debuggers │
└──────────────────┴─────────────────────────────┴─────────────────────────┘

The Heisenbug: The Most Infuriating Bug in Existence

Named after Heisenberg’s uncertainty principle, a Heisenbug is a bug that disappears or changes behavior when you try to observe it. You add a breakpoint — it doesn’t trigger. You add a console.log — it stops happening.

Classic causes:

  • The timing introduced by your debugger/logger changes a race condition
  • Optimizations disabled in debug mode
  • Side effects from measurement code
  • Production-only builds behave differently from debug builds
// Classic Heisenbug scenario — race condition
async function fetchUserAndPosts(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(userId); // Bug: race condition sometimes
  return { user, posts };
}

// When you add logging, the await slightly changes timing:
async function fetchUserAndPosts(userId) {
  console.log('fetching user...'); // ← This tiny delay "fixes" the race condition
  const user = await fetchUser(userId);
  console.log('fetching posts...');
  const posts = await fetchPosts(userId); // Appears to work now
  return { user, posts };
}

// The REAL fix:
async function fetchUserAndPosts(userId) {
  const [user, posts] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId)
  ]);
  return { user, posts };
}

Rule: If removing your logging “fixes” the bug, you have a Heisenbug. Look for concurrency and timing issues.

The Bohr Bug vs The Mandelbug

Bug TypeBehaviorApproach
Bohr BugConsistent, reproducibleStandard debugging
HeisenbugDisappears when observedAdd non-intrusive logging
MandelbugComplex, chaotic causesSimplify the system
SchrödinbugWorks until someone reads the codeIt shouldn’t have worked ever

A Schrödinbug is a personal favorite: code that shouldn’t work by any logic, but runs fine in production for years — until someone reads it and says “wait, this is wrong” and suddenly it starts failing.


4. Rubber Duck Debugging & Other Weird Techniques That Work

The Rubber Duck

The concept is simple and backed by real cognitive science: explain your code, line by line, to an inanimate object.

You:  "Okay, so this function takes a user ID..."
Duck: [silence]
You:  "...and fetches the user from the database..."
Duck: [silence]
You:  "...then it checks if the user is an admin..."
Duck: [silence]
You:  "...wait. It checks `user.role === 'admin'` but I'm storing roles
       as `user.permissions.role` after the migration."
Duck: [silence, but somehow wise]
You:  "Oh my god."

Why it works: Explaining something forces you to construct it linearly, which surfaces assumptions you weren’t aware you were making. Your brain fills in gaps silently when reading; it can’t when speaking.

Modern rubber ducks: An AI assistant, a coworker (willing or not), a blog post you’re writing, the cat.

Change Isolation (Binary Search for Bugs)

If the bug appeared sometime in the last 100 commits, you don’t read all 100 commits. You use binary search:

# Git bisect — the most underused git feature

git bisect start
git bisect bad HEAD          # Current commit is broken
git bisect good v2.1.0       # This tag was working fine

# Git checks out the midpoint commit automatically
# You test it, then:
git bisect good   # or
git bisect bad

# Git narrows down automatically (log₂(100) ≈ 7 steps to find it)
# When done:
git bisect reset

# BONUS: Automate bisect with a test script
git bisect run npm test -- --grep "checkout flow"
# Git runs your test on each commit and bisects automatically

This narrows 100 commits down to the culprit in ~7 steps. Every developer should know this exists.

The “Explain It to a Junior Dev” Technique

Different from rubber duck because you’re imagining an audience that will ask questions. This forces you to be even more explicit.

Write out your explanation as if you’re submitting a Stack Overflow question. In doing so, you often answer your own question before hitting “Post” — so much so that Stack Overflow calls this phenomenon “rubber duck” questions.

Change One Thing at a Time (The Discipline That Saves Hours)

❌ The impatient approach:
    Identify 3 possible issues
    → Fix all three at once
    → Bug disappears
    → You don't know which fix worked
    → Bug comes back in a different form
    → You have no idea why

✅ The disciplined approach:
    Identify 3 possible issues
    → Fix issue #1
    → Test
    → Doesn't fix it? Revert #1
    → Fix issue #2
    → Test
    → Bug fixed. You understand WHY.
    → You write a regression test.

5. Reading Stack Traces Like a Pro

A stack trace is not an error message — it’s a map. Most developers read the first line and stop. The pros read the whole thing.

Anatomy of a Stack Trace

Error: Cannot read properties of undefined (reading 'email')
    at formatUserProfile (user-profile.js:47:32)    ← Where the crash happened
    at renderDashboard (dashboard.js:112:18)         ← Called by this
    at processTicksAndRejections (node:internal/process/task_queues:95:5)

    └── Node.js internal — usually not your bug, stop here

        ┌──────────────┬──────────────┬────────┬──────────┐
        │  Function    │  File        │  Line  │  Column  │
        └──────────────┴──────────────┴────────┴──────────┘

Reading strategy:
1. Read the ERROR TYPE + MESSAGE first → "undefined has no .email"
2. Find YOUR code in the stack (ignore node_modules, internals)
3. The top-most frame in YOUR code is where it crashed
4. Read DOWN to see what called it (the "who caused the situation")
5. The bottom of your code in the stack is the origin of the problem

Real Example: Dissecting a React Error

Uncaught TypeError: Cannot read properties of null (reading 'map')
    at ProductList (ProductList.jsx:23)
    at renderWithHooks (react-dom.development.js:14985)
    at mountIndeterminateComponent (react-dom.development.js:17811)
    ...

Diagnosis:
  Error: "null" doesn't have .map → we're calling .map on null
  Location: ProductList.jsx, line 23
  Likely cause: products prop is null (not just empty array [])

  Go to line 23:
    const items = products.map(p => <ProductCard key={p.id} product={p} />);

  The question is: WHERE does products come from, and when is it null?
  → Check the parent component. Is the API call loading?

Fix:
  const items = (products ?? []).map(p => <ProductCard key={p.id} product={p} />);
  // Or add a loading state check before rendering ProductList

Minified Stack Traces (Production)

Production JavaScript is minified. Stack traces become:

TypeError: Cannot read properties of undefined (reading 'n')
    at t.e (main.8f3a2c1d.js:1:28945)
    at main.8f3a2c1d.js:1:13422

This is useless without a source map.

# Generate source maps in your build:

# webpack.config.js
module.exports = {
  devtool: 'source-map',  // Generates .map files
  // For production (keep maps private):
  devtool: 'hidden-source-map'  // Maps exist but not referenced publicly
}

# Vite — vite.config.js
export default {
  build: {
    sourcemap: true  // or 'hidden'
  }
}

# Upload source maps to error tracking (Sentry, Datadog):
npx @sentry/cli sourcemaps upload --org my-org --project my-project ./dist

6. The Art of the Minimal Reproduction

If you can reproduce a bug in 10 lines of code, you can fix it in 10 minutes. If you can only reproduce it in your full application, you might spend 10 hours.

The Minimum Viable Bug

// Bug: "The currency formatting is wrong for amounts over $999"

// ❌ The lazy report:
// "It's broken in the checkout flow somewhere"

// ✅ The minimal reproduction:
const Intl = require('intl');

function formatCurrency(amount) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  }).format(amount);
}

console.log(formatCurrency(999));   // "$999.00" ✅
console.log(formatCurrency(1000));  // "$1,000.00" ← Wait, this is correct
console.log(formatCurrency(999.9)); // "$999.90" ← Hmm, also correct

// Turns out the bug was in the COMPARISON, not the formatting:
if (amount > 999) {  // Bug: should be >= 1000
  applyBulkDiscount();
}

Minimal reproductions help because:

  • They force you to understand the bug deeply
  • They eliminate all the unrelated noise
  • They make the bug trivially shareable
  • They often reveal the fix as you build them

How to Build a Minimal Reproduction

Step 1: Start with the failing test case
  → What inputs trigger the bug?
  → What output do you expect? What do you get?

Step 2: Strip away everything unrelated
  → Remove authentication
  → Remove database (use hardcoded data)
  → Remove UI (pure logic)
  → Remove network (use mocked responses)

Step 3: Can you reproduce it in a single file?
  → Aim for < 50 lines
  → If not, you haven't isolated it yet

Step 4: Create a shareable environment
  → CodeSandbox / StackBlitz / JSFiddle
  → A GitHub gist
  → A single runnable script

Step 5: Verify the minimal reproduction still shows the bug
  → You'd be surprised how often it doesn't — and you've solved it!

7. Debugger Tools You Should Actually Be Using

Most developers use console.log. Some developers use a debugger. The difference in debugging speed is enormous.

Chrome DevTools: The Full Power

// You probably know this:
console.log('value:', myVar);

// But do you know these?

// 1. Conditional breakpoints (right-click a breakpoint in DevTools)
// Only pauses when: userId === 'abc123'
// No code changes needed

// 2. Logpoints (log without stopping execution)
// Right-click line → Add logpoint → "User: {user.name}, role: {user.role}"

// 3. console.table — for arrays of objects
console.table(users);
// Renders a sortable, filterable table in the console

// 4. console.time — measure exactly how long code takes
console.time('api-call');
await fetchData();
console.timeEnd('api-call'); // "api-call: 342.51ms"

// 5. console.trace — see where a function is being called from
function doSomething() {
  console.trace('doSomething called from:');
}

// 6. console.assert — only logs if condition is false
console.assert(user.role === 'admin', 'Expected admin, got:', user.role);

// 7. debugger statement — works anywhere, even without DevTools open
function suspiciousFunction(data) {
  debugger; // ← Execution pauses here when DevTools is open
  return process(data);
}

The Watch Expressions Panel

Instead of adding console.log for every variable, add them as watch expressions in DevTools. They update in real-time as you step through code.

DevTools → Sources → Watch panel → (+) Add expression:

Expressions to watch:
├── user.permissions
├── cart.items.length
├── typeof response
├── JSON.stringify(state, null, 2)  ← Pretty prints state object
└── new Date(timestamp).toISOString()  ← Decode timestamps

Node.js Debugger

# Start Node.js with debugger
node --inspect server.js
node --inspect-brk server.js  # --brk pauses at first line

# Then open: chrome://inspect in Chrome
# Or use VS Code's built-in debugger (even better)

VS Code Debugging (The Setup That Pays Off)

// .vscode/launch.json — commit this to your repo!
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Node.js",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/src/index.js",
      "env": {
        "NODE_ENV": "development",
        "DEBUG": "*"
      }
    },
    {
      "name": "Debug Jest Tests",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "node",
      "runtimeArgs": [
        "--experimental-vm-modules",
        "${workspaceFolder}/node_modules/.bin/jest",
        "--runInBand",           // Run tests serially (debugger-friendly)
        "--watchAll=false"
      ],
      "console": "integratedTerminal"
    },
    {
      "name": "Debug Chrome",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/src"
    }
  ]
}

With this config, you set breakpoints in VS Code, press F5, and debug directly in your editor. No Chrome DevTools switching required.


8. printf Debugging Done Right

Sometimes a debugger isn’t available or practical (containerized apps, serverless, async race conditions). Log-based debugging is perfectly valid — but most developers do it badly.

Structured Logging

// ❌ The log that tells you nothing useful in production
console.log('user data:', user);
console.log('here');
console.log('got to this point');

// ✅ Structured logging that gives you context
const log = {
  info: (msg, meta = {}) => console.log(JSON.stringify({
    level: 'info',
    timestamp: new Date().toISOString(),
    message: msg,
    ...meta
  })),
  error: (msg, meta = {}) => console.error(JSON.stringify({
    level: 'error',
    timestamp: new Date().toISOString(),
    message: msg,
    ...meta
  }))
};

// Usage:
log.info('Processing payment', {
  userId: user.id,
  orderId: order.id,
  amount: order.total,
  currency: order.currency
});

// Output (parseable by log aggregators like Datadog, Splunk):
// {"level":"info","timestamp":"2024-06-15T14:32:11.234Z","message":"Processing payment","userId":"usr_abc123","orderId":"ord_xyz789","amount":59.99,"currency":"USD"}

Using the debug Package (Node.js)

// npm install debug
const debug = require('debug');

// Create namespaced loggers
const dbDebug = debug('app:database');
const apiDebug = debug('app:api');
const authDebug = debug('app:auth');

// Only logs when DEBUG env var matches
dbDebug('Connecting to database: %s', dbUrl);
apiDebug('Incoming request: %o', { method: 'GET', path: '/users' });

// Enable specific namespaces:
// DEBUG=app:database node server.js      → Only DB logs
// DEBUG=app:*        node server.js      → All app logs
// DEBUG=*            node server.js      → Everything
// DEBUG=app:*,-app:database node server.js → All except DB

// Zero performance cost when disabled (no-op function)

Strategic Log Placement

async function processOrder(orderId) {
  // Log ENTRY with key identifiers
  log.info('processOrder: start', { orderId });

  const order = await db.orders.findById(orderId);

  // Log key decision points
  if (!order) {
    log.error('processOrder: order not found', { orderId });
    throw new OrderNotFoundError(orderId);
  }

  log.info('processOrder: order found', {
    orderId,
    status: order.status,
    itemCount: order.items.length,
    total: order.total
  });

  // Log BEFORE external calls (so you know if the call was made)
  log.info('processOrder: calling payment provider', {
    orderId,
    provider: 'stripe',
    amount: order.total
  });

  const payment = await stripe.charge(order);

  // Log AFTER external calls with the result
  log.info('processOrder: payment result', {
    orderId,
    paymentId: payment.id,
    status: payment.status
  });

  // Log EXIT
  log.info('processOrder: complete', { orderId });
  return payment;
}

9. Debugging in Production (Without Panicking)

Production bugs are different. You can’t just attach a debugger. Users are affected. Someone’s boss might be calling.

The Incident Response Mindset

FIRST: Stabilize, THEN Investigate

Step 1: Assess severity (1 minute)
  ├── Is the entire service down? → Immediate rollback
  ├── Is one feature broken? → Disable feature flag if possible
  └── Is it a data issue? → Stop writes if at risk of corruption

Step 2: Communicate (2 minutes)
  └── Post in #incidents: "Investigating reports of [X]. Will update in 10 min."
      → Don't make people chase you for status

Step 3: Gather evidence (before changing anything!)
  ├── Check error rates in monitoring (Datadog, Sentry, etc.)
  ├── Check recent deployments (was there a deploy in the last hour?)
  ├── Check recent infrastructure changes
  └── Check external dependencies (is Stripe/Auth0/AWS down?)
      → https://www.githubstatus.com, https://status.aws.amazon.com

Step 4: Investigate with read-only queries first
  └── Never run UPDATE/DELETE without SELECT first

Step 5: Fix or mitigate
  ├── Rollback if cause is clear and recent deploy is culprit
  ├── Hotfix for well-understood bugs
  └── Feature flag disable for isolated features

Step 6: Post-mortem (blameless)
  └── What happened → Why it happened → What prevents recurrence

Feature Flags for Instant Rollback

// Using a feature flag service (LaunchDarkly, Unleash, etc.)
// Or a simple database/config-based approach:

async function processCheckout(cart, user) {
  // Check if new checkout flow is enabled for this user
  const useNewCheckout = await flags.isEnabled('new-checkout-v2', {
    userId: user.id,
    userSegment: user.plan
  });

  if (useNewCheckout) {
    return await newCheckoutFlow(cart, user); // New code
  } else {
    return await legacyCheckoutFlow(cart, user); // Safe fallback
  }
}

// When new-checkout-v2 bugs in production:
// → Flip flag to false in dashboard (no deploy needed)
// → 100% of users instantly on legacy flow
// → No rollback deployment required
// → Fix the bug, re-enable flag gradually (5% → 25% → 100%)

Distributed Tracing

In microservices, the bug might not be where the error surfaces.

// Using OpenTelemetry for distributed tracing

const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('checkout-service');

async function processOrder(orderId) {
  return tracer.startActiveSpan('processOrder', async (span) => {
    span.setAttribute('orderId', orderId);

    try {
      const order = await tracer.startActiveSpan('db.findOrder', async (dbSpan) => {
        const result = await db.orders.findById(orderId);
        dbSpan.setAttribute('found', !!result);
        dbSpan.end();
        return result;
      });

      const payment = await tracer.startActiveSpan('stripe.charge', async (paySpan) => {
        const result = await stripe.charge(order);
        paySpan.setAttribute('payment.status', result.status);
        paySpan.end();
        return result;
      });

      span.setStatus({ code: SpanStatusCode.OK });
      return payment;
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      throw error;
    } finally {
      span.end();
    }
  });
}

// Now in Jaeger/Zipkin/Datadog APM, you can see the FULL trace:
// processOrder (350ms)
//   └── db.findOrder (45ms)
//   └── stripe.charge (305ms) ← HERE'S YOUR BOTTLENECK / FAILURE POINT

10. The Top 10 Bugs That Haunt Every Developer

These bugs have ended careers (not really, but it felt like it).

1. The Off-By-One Error

// Count: [0, 1, 2, 3, 4] — 5 items
const items = ['a', 'b', 'c', 'd', 'e'];

// ❌ Bug: misses last item
for (let i = 0; i < items.length - 1; i++) {
  process(items[i]);
}

// ❌ Bug: index out of bounds
for (let i = 0; i <= items.length; i++) {
  process(items[i]); // items[5] is undefined
}

// ✅ Correct
for (let i = 0; i < items.length; i++) {
  process(items[i]);
}
// Or just:
items.forEach(item => process(item));

// ❌ Classic pagination bug
const page = 1;
const pageSize = 10;
const offset = page * pageSize; // = 10, skips first 10 items!

// ✅ Correct
const offset = (page - 1) * pageSize; // = 0 for page 1

2. Floating Point Arithmetic

// The bug that has surprised every developer at least once
0.1 + 0.2 === 0.3 // false

console.log(0.1 + 0.2); // 0.30000000000000004

// ❌ Comparing floats directly
if (price === 0.30) { ... } // Never do this

// ✅ For money: use integers (cents)
const priceInCents = 30; // Store as 30, not 0.30
const displayPrice = (priceInCents / 100).toFixed(2); // "0.30"

// ✅ For other floats: use epsilon comparison
const EPSILON = 0.000001;
Math.abs(a - b) < EPSILON // "Are a and b basically equal?"

// ✅ Use a library: decimal.js, bignumber.js
import Decimal from 'decimal.js';
new Decimal('0.1').plus('0.2').equals('0.3'); // true

3. The Async/Await Trap

// ❌ Bug: not awaiting a Promise
async function saveUser(userData) {
  db.users.save(userData); // Missing await! Returns a Promise, not the result
  return { success: true }; // Returns before save completes
}

// ❌ Bug: await in loop (sequential when should be parallel)
async function fetchAllUsers(userIds) {
  const users = [];
  for (const id of userIds) {
    const user = await fetchUser(id); // Waits for each one ← SLOW
    users.push(user);
  }
  return users;
}

// ✅ Parallel fetching
async function fetchAllUsers(userIds) {
  return Promise.all(userIds.map(id => fetchUser(id)));
}

// ❌ Bug: unhandled promise rejection
fetchData().then(data => process(data));
// If fetchData() rejects, error is silently swallowed

// ✅ Always handle errors
fetchData()
  .then(data => process(data))
  .catch(error => log.error('fetchData failed', { error }));

// Or with async/await:
try {
  const data = await fetchData();
  process(data);
} catch (error) {
  log.error('fetchData failed', { error });
}

4. Mutating Shared State

// ❌ Bug: function mutates its input
function applyDiscount(cart, discountPercent) {
  cart.total = cart.total * (1 - discountPercent / 100); // Mutates!
  return cart;
}

const cart = { total: 100, items: [] };
const discounted = applyDiscount(cart, 10);
console.log(cart.total); // 90 — original was modified!

// ✅ Return a new object
function applyDiscount(cart, discountPercent) {
  return {
    ...cart,
    total: cart.total * (1 - discountPercent / 100)
  };
}

// ❌ Classic JavaScript array mutation trap
const nums = [3, 1, 4, 1, 5];
const sorted = nums.sort(); // Mutates nums!
console.log(nums); // [1, 1, 3, 4, 5] — original changed

// ✅ Sort a copy
const sorted = [...nums].sort((a, b) => a - b);

5. The Timezone Bug

// The bug that only shows up in production (when users are in other timezones)

// ❌ Bug: treating date strings as local time
const date = new Date('2024-06-15'); // Interpreted as UTC midnight
// In New York (UTC-5): date.getDate() returns 14, not 15!

// ❌ Bug: storing timezone-naive datetimes
const appointmentTime = '2024-06-15 14:00:00'; // What timezone??

// ✅ Always store and transmit in UTC
const now = new Date().toISOString(); // "2024-06-15T19:00:00.000Z" ← UTC explicit

// ✅ Use date-fns or Temporal API for date arithmetic
import { addDays, format, parseISO } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';

const meeting = parseISO('2024-06-15T19:00:00Z');
const localTime = formatInTimeZone(meeting, 'America/New_York', 'h:mm a zzz');
// "3:00 PM EDT"

// ✅ The Temporal API (modern browsers, 2024+)
const dt = Temporal.ZonedDateTime.from('2024-06-15T14:00:00[America/New_York]');
dt.toInstant().toString(); // Convert to UTC for storage

6. The null vs undefined vs "" vs 0 vs false Confusion

// All of these are "falsy" in JavaScript:
// false, 0, "", null, undefined, NaN

// ❌ Bug: treating all falsy values the same
function getUsername(user) {
  return user.name || 'Anonymous';
}
getUsername({ name: '' }); // 'Anonymous' — but maybe '' is valid!
getUsername({ name: 0 }); // 'Anonymous' — 0 is a weird username but...

// ✅ Be explicit about what you're checking
function getUsername(user) {
  return user.name ?? 'Anonymous'; // Only fallback for null/undefined
}
getUsername({ name: '' }); // '' — empty string preserved
getUsername({ name: null }); // 'Anonymous' ✅

// ❌ Bug: optional chaining confusion
const city = user?.address?.city;
// city is undefined if user is null OR if address is null OR if city doesn't exist
// You can't tell WHICH one caused the undefined

// ✅ Be explicit in your fallbacks
const city = user?.address?.city ?? 'City not specified';

11. Preventing Bugs Before They’re Born

The best bug is the one that never makes it into the codebase.

Types as Bug Prevention

// TypeScript: catch entire categories of bugs at compile time

// ❌ JavaScript: runtime error
function formatPrice(price) {
  return `$${price.toFixed(2)}`; // Crashes if price is string "9.99"
}
formatPrice("9.99"); // TypeError at runtime

// ✅ TypeScript: caught at compile time
function formatPrice(price: number): string {
  return `$${price.toFixed(2)}`;
}
formatPrice("9.99"); // ← TypeScript error: Argument of type 'string' is not assignable to parameter of type 'number'

// ✅ Stronger types with branded types
type UserId = string & { readonly brand: unique symbol };
type ProductId = string & { readonly brand: unique symbol };

function getUser(id: UserId) { /* ... */ }

const userId = "usr_123" as UserId;
const productId = "prod_456" as ProductId;

getUser(userId);   // ✅
getUser(productId); // ← TypeScript error! Can't mix up IDs

Property-Based Testing

Instead of testing specific examples, test invariants (properties that should always be true).

// npm install fast-check

import fc from 'fast-check';

// ❌ Example-based test: only tests 3 cases
test('sort returns sorted array', () => {
  expect(sort([3,1,2])).toEqual([1,2,3]);
  expect(sort([5,4])).toEqual([4,5]);
  expect(sort([])).toEqual([]);
});

// ✅ Property-based test: tests 100 random inputs
test('sort invariants always hold', () => {
  fc.assert(fc.property(
    fc.array(fc.integer()),
    (arr) => {
      const sorted = sort([...arr]);

      // Property 1: Same length
      expect(sorted).toHaveLength(arr.length);

      // Property 2: All elements present
      expect(sorted.sort()).toEqual([...arr].sort());

      // Property 3: Actually sorted
      for (let i = 0; i < sorted.length - 1; i++) {
        expect(sorted[i]).toBeLessThanOrEqual(sorted[i + 1]);
      }
    }
  ));
});
// fast-check automatically finds edge cases: [0], [-1,0], [MAX_INT]

Code Review as Bug Prevention

Effective Code Review Checklist:

Logic & Correctness:
├── Does the code do what it says it does?
├── Are edge cases handled? (empty, null, max values)
├── Are error cases handled?
└── Are there any off-by-one risks?

Security:
├── Is user input sanitized before use in DB/HTML?
├── Are any secrets hardcoded?
└── Are permissions/auth checked at the right layer?

Performance:
├── Any N+1 query problems?
├── Are large datasets paginated?
└── Any potential memory leaks?

Maintainability:
├── Would a new team member understand this in 6 months?
├── Are complex sections commented?
└── Is naming clear and intentional?

12. When to Ask for Help (And How to Do It Well)

Knowing when to ask for help is a skill. Knowing how to ask is an art.

The 30-Minute Rule

Spend 30 minutes genuinely trying to solve a bug before asking for help. This time forces you to:

  • Form a coherent explanation of the problem
  • Try the obvious solutions
  • Gather relevant information

After 30 minutes with no progress: ask. Struggling alone for 3 hours is not productive — it’s just pride.

How to Ask a Good Question

Bad question:
"My code doesn't work, can anyone help?"

Good question:
"I'm getting a 'Cannot read property of undefined' error in my
checkout flow, specifically in the `applyDiscount()` function.

**What I expected:** The function returns the discounted price.
**What I got:** `TypeError: Cannot read properties of undefined (reading 'total')`

**Stack trace:**

TypeError: Cannot read properties of undefined (reading ‘total’) at applyDiscount (checkout.js:47)


**Relevant code:**
```javascript
function applyDiscount(cart, discount) {
  return cart.total * (1 - discount); // Line 47
}

What I’ve tried:

  • Logging cart before line 47 — it logs undefined
  • Checking where applyDiscount is called — the cart object seems valid there

I think the cart is getting undefined somewhere between the call site and the function, but I can’t see how.”


The good question gives:
- Context (what is this code doing)
- Expected vs actual behavior
- The error message and stack trace
- The relevant code (not the whole file)
- What you've already tried

---

## 13. The Debugging Mindset: A Philosophy

After all the tools and techniques, the most important thing is how you *think* about bugs.

### Bugs Are Information, Not Failures

Every bug is your codebase telling you something true about the world that your mental model got wrong. The bug isn't lying. The compiler isn't lying. The database isn't lying.

*You* believed something that wasn't true. The bug showed you what was actually true.

This reframing changes everything. Instead of "why is this stupid thing broken?" it becomes "what is this showing me about my assumptions?"

### The Bug Was Always There

Your code doesn't suddenly become buggy. The bug was there when you wrote it — you just hadn't found the input that exposed it yet. This means:

- Bugs in production aren't bad luck; they're undiscovered bugs from development
- Every bug fixed is an opportunity to add a test that would have caught it
- The best time to write that test was before the bug; the second best time is now

### On the Joy of Debugging

Here's a secret: debugging is *fun* when you stop fighting it. It's a logic puzzle. A mystery. The code is Sherlock Holmes' crime scene, and you are the detective.

Every clue matters. The error message is a clue. The timing is a clue. Which users are affected is a clue. What was deployed last is a clue. The behavior under different inputs is a clue.

When you find the bug — especially a devious one — the satisfaction is real. That "AHA!" moment is one of the genuine pleasures of software engineering.

Enjoy the hunt.

“The most effective debugging tool is still careful thought, coupled with judiciously placed print statements.” — Brian Kernighan


---

## Quick Reference: The Debugging Cheat Sheet

┌──────────────────────────────────────────────────────────────────┐ │ DEBUGGING QUICK REFERENCE │ ├──────────────────┬───────────────────────────────────────────────┤ │ Bug disappeared │ Heisenbug — look for race conditions │ │ Works locally │ Check: env vars, node version, timezone, OS │ │ Appeared lately │ git bisect to find the culprit commit │ │ Can’t reproduce │ Add structured logging, capture more context │ │ Too complex │ Build a minimal reproduction │ │ Stuck >30 min │ Rubber duck it, then ask for help │ │ Production down │ Stabilize first, investigate second │ │ Off-by-one? │ Check every loop, slice, and index carefully │ │ Async weirdness │ Check: missing await, unhandled rejection │ │ NaN / undefined │ Check: null coalescing, type coercion │ ├──────────────────┴───────────────────────────────────────────────┤ │ Remember: The bug is not lying. Your assumptions might be. │ └──────────────────────────────────────────────────────────────────┘


---

*Found a bug in this article? The irony would be appreciated. Open a PR.*

*Tags: `debugging` `developer-tools` `javascript` `best-practices` `software-engineering`*