Dataslope logoDataslope

Debugging and Reasoning About Programs

How to find bugs systematically — and how to write code you can reason about in the first place.

Every programmer spends more time reading and debugging code than writing it. The good news is that debugging is a skill — not a talent — and it gets dramatically easier once you have a process.

This chapter covers two things:

  1. Reasoning about code so you understand what it should do.
  2. Debugging code that isn't doing what you thought.

What debugging really is

A bug exists because there's a gap between:

  • the mental model you have of what the program does, and
  • what the program actually does when it runs.

Debugging is the process of finding where and why those two differ. You're not "fixing the code"; you're updating your model until it matches reality, then fixing the code.

If you find yourself thinking "this should work" — you've already discovered the bug. The gap between "should" and "does" is the entire problem.

A scientific-method approach

When something goes wrong, resist the urge to start changing code. Instead, follow a tiny loop:

The biggest beginner mistake is jumping straight to fix. The person who slows down at "form a hypothesis" finishes faster.

The most useful debugging tool: console.log

It's not glamorous, but console.log solves more bugs than any debugger. The trick is to print what you assumed, not the final answer.

Code Block
JavaScript ES2023+

If you log the types of the inputs and the running total, the bug screams at you:

Code Block
JavaScript ES2023+

The third iteration shows total becoming the string "64" — that's the moment something went wrong. Now you know where.

Better than console.log: structured logging

A few small habits make logs much more useful:

  • Label what you're printing: console.log("user:", user) is far better than console.log(user) when 10 logs appear at once.
  • Group related logs: console.group("processing order", id)
    • console.groupEnd().
  • Use console.table for arrays of objects:
Code Block
JavaScript ES2023+

Reading a stack trace

When something throws, JS prints a stack trace — a snapshot of the call stack at the moment of the error.

Code Block
JavaScript ES2023+

Read stack traces top to bottom:

  1. The message tells you what went wrong.
  2. The top frame tells you where it threw.
  3. The lower frames tell you how the program got there.

If a function appears in the trace but you didn't call it directly, the line above it called it. Walk up until you find your code (vs. library code) — that's usually the most informative line.

Common error patterns

A small zoo of errors you'll meet often:

Code Block
JavaScript ES2023+

A pattern: errors usually tell you exactly what went wrong if you read them. "Cannot read property X of undefined" means "something I expected to be an object was undefined — and I tried to read X from it." Find what that "something" is.

Defensive thinking: invariants

An invariant is a fact about your program that should always be true at a given point. Stating them in your head — or in comments, assertions, or tests — gives you anchors.

Code Block
JavaScript ES2023+

When a bug appears, ask: which invariant did I think held that actually didn't? That question, asked out loud, often is the debugging session.

Rubber-duck debugging

The classic trick: explain the code, line by line, out loud, to an inanimate object — traditionally a rubber duck. The act of articulating "and then this returns... wait, no it doesn't" catches a huge fraction of bugs.

You don't need a duck. You just need to slow down enough to describe what you think is happening.

Binary search bug-hunting

When the bug is "somewhere in this big code path", don't read top to bottom — bisect.

  1. Pick a point in the middle.
  2. Print or check: is the state correct here?
  3. If yes, the bug is after this point. If no, it's before.
  4. Repeat in the smaller half.

This narrows down even huge codebases in a handful of steps.

Reproducing the bug

The single most powerful debugging technique: shrink the failing case to the smallest reliably-reproducing example.

  • "My app crashes sometimes" — useless.
  • "My app crashes when I open the user page" — better.
  • "It crashes in formatDate when a user has no birthday field, but not otherwise" — that's a fix request, not a bug report.

When you can reproduce a bug on demand, you've already done 80% of the work.

Writing code that's easier to reason about

The best debugging is not needing to debug. A few habits that slash bug rates:

  1. Small functions. A 5-line function is easier to verify correct than a 50-line one.
  2. Pure when possible. As you saw in the FP chapter, side effects make code harder to reason about.
  3. Name things precisely. data and result say nothing. filteredOrders and revenueByMonth say a lot.
  4. Use const by default. A variable that never changes can't secretly be the wrong value somewhere.
  5. Guard at the edges. Validate inputs, then trust them inside.
  6. Don't be clever. Clear code beats clever code. The code you write today, your future self has to read tomorrow.

Multi-file: a bug-hunting walk-through

This module has a subtle bug. Run it, then read the code and find the bug before looking at the fix below.

Code Block
JavaScript ES2023+

Symptoms you'd notice:

  • The total is correct on the first call.
  • But run applyDiscounts(cart, 0.1) again and the total drops further every time — because each call mutates item.price.

The fix: don't mutate the caller's data.

function applyDiscounts(cart, discount) {
  return cart.reduce(
    (total, item) => total + item.price * (1 - discount) * item.qty,
    0,
  );
}

This is a classic immutability bug — the same one we warned about in the functional intuition chapter.

Challenge

Challenge
JavaScript ES2023+
Find and fix the bug

Below is a function average(nums) that is supposed to return the arithmetic mean of an array of numbers, ignoring any non-number entries (strings, nulls, etc.). It has at least one bug.

Your task:

  • Identify the issue(s) by reasoning about the code.
  • Replace the function with a correct implementation.
  • It must return 0 for an empty array AND for an array with no valid numbers (avoid NaN from dividing by zero).
  • It must not mutate the input array.

QuestionSelect one

A function you wrote is returning the wrong number. You've stared at the code for ten minutes. What's the most effective next step?

Rewrite the function from scratch and hope the new version is correct

Search the web for "JavaScript function wrong number"

Add logs that print your inputs, intermediate values, and assumptions — then re-run to see where the actual values diverge from what you expected

Add try/catch around the call to suppress the error

On this page