Debugging: Finding Why Things Broke

📌 Concept 13 of 13✍️ Written by Mark Sullivan📅 Reviewed 2026-04-23⏱ ~11 min read

Debugging is most of programming. Beginners think bugs are setbacks; experienced developers know they're the work.

Bee, our debugging mascot

From Bee: Read the LAST line of the error first. That is the actual error. The lines above it are just the path the program took to get there.

What Is Debugging?

Debugging is figuring out why code didn't do what you expected, then fixing it. The skill is forming a hypothesis ("I think the variable is undefined when this runs"), testing it (print the variable, run again), and iterating.

Read the Error Message

Beginners often ignore error messages because they look scary. They're not — they tell you exactly what's wrong and on which line. Read the last line first (the actual error), then the line numbers above it (the path of how you got there).

Print Debugging

The simplest, most underrated technique: add console.log / print at suspect points to see what variables actually contain. Yes, professional debuggers exist; no, they're not always faster than print.

Binary Search the Bug

If a 100-line function is broken, comment out the bottom half — does the bug still appear? If yes, the bug is in the top half. Cut that half in half. Repeat. You'll find the line in ~7 cuts.

Spot the Bug

A new buggy snippet every time. Click the line you think is wrong.

Code Examples in Three Languages

// Print debugging
function calculate(items) {
  console.log("items:", items);
  let total = 0;
  for (const item of items) {
    console.log("processing:", item);
    total += item.price;
  }
  console.log("final total:", total);
  return total;
}

// Browser DevTools also lets you set breakpoints
// Press F12, go to Sources, click a line number
# Print debugging
def calculate(items):
    print("items:", items)
    total = 0
    for item in items:
        print("processing:", item)
        total += item["price"]
    print("final total:", total)
    return total

# Use the built-in debugger
import pdb; pdb.set_trace()  # pauses execution here
// Print debugging
public static int calculate(List<Item> items) {
    System.out.println("items: " + items);
    int total = 0;
    for (Item item : items) {
        System.out.println("processing: " + item);
        total += item.price;
    }
    System.out.println("final total: " + total);
    return total;
}

// IDEs (IntelliJ, Eclipse) provide rich debuggers

Best Practices

  1. Reproduce reliably first. A bug you can reproduce on demand is half-fixed.
  2. Change one thing at a time. Otherwise you don't know what fixed it.
  3. Read the stack trace top-to-bottom. Find the first line in your code.
  4. Rubber-duck it. Explain the code aloud to an inanimate object — bugs often surface in the explanation.
  5. Take breaks. Bugs that vanish after a walk are real.

Common Mistakes

  • Assuming the error message is wrong. It almost never is.
  • Changing things "to see what happens." Form a hypothesis first.
  • Skipping the simplest possibility. Did you save the file? Restart the server?
  • Not using version control. Without it, "I'll just try this" can lose hours of work.
🐛
See the bugs in action

We have a dedicated Common Bugs with Debugging page — five real broken snippets with the fix. Read it before you start writing.

How It Works Under the Hood

A debugger is a privileged program that controls another program's execution. It can pause at a line (breakpoint), inspect memory, change variables, and resume. Modern debuggers attach via the language's debug API (V8's Inspector Protocol, Python's pdb, JVM's JDI).

A stack trace is a snapshot of the call stack at the moment of the error: the chain of function calls that led there. The deepest frame is the one that threw; reading top-to-bottom shows you the path. Reading bottom-up shows you the trigger.

"Print debugging" still works because logs are reproducible. A debugger session is ephemeral; printed logs persist and can be diffed across runs. Both are valid; the right tool depends on whether the bug is local (debugger wins) or distributed (logs win).

From Beginner to Pro

The same concept gets deeper as you grow. Here's what mastery looks like at three levels:

Beginner

You add console.log / print and read the error message.

Intermediate

You use breakpoints, step-over / step-into, inspect variables in the IDE.

Pro

You write minimal repros, bisect with git, log structurally (JSON), correlate request IDs across services, and do post-mortems that produce action items, not blame.

Performance & Gotchas

  • Changing two things at once — you don't know which fix mattered.
  • Debugging on production data — corruption + lost reproduction.
  • Forgetting to remove debug logs — leaked secrets in prod logs.
  • "Works on my machine" — reproduce in a clean container before declaring done.

Quick Quiz

Real-World Uses (Production Code, Today)

Production incident

When something breaks at 3am, the on-call engineer reads the stack trace, forms a hypothesis, tests it. Same skill as your first "Hello, world" debug.

Bisecting a bug with git

git bisect automates the binary-search-the-bug technique. The technique came first; the tool just speeds it up.

Customer support escalation

"User says save button is broken." Reproduce → check logs → find the cause → fix. Debugging is a job description as much as a skill.

Frequently Asked Questions

What's a stack trace?

The list of function calls that led to the error. Read it top-down to find the path.

What's a breakpoint?

A line in your code where the debugger pauses execution so you can inspect variables.

Why does my code work locally but break in production?

Common causes: different environment variables, different data, race conditions, different versions of dependencies.

What's "rubber-duck debugging"?

Explaining your code line by line to a rubber duck (or any inanimate object). Often the bug becomes obvious as you talk.

How do I debug an asynchronous bug?

Add timestamps to your logs and look at the order of events. async bugs are usually about ordering or timing.

What's the difference between a bug and an error?

Errors are explicit complaints from the runtime. Bugs are when the code runs but does the wrong thing.

When should I use a debugger vs print?

Print is faster for small bugs. A debugger pays off when you need to inspect many variables or step line by line.

Are there bugs that "just go away"?

No. They're hiding. Find the cause or it'll resurface at the worst time.

Key takeaways
  • Debugging: Finding Why Things Broke is one of the 13 universal concepts of programming.
  • The syntax differs across languages, but the underlying idea is the same.
  • Practice in the playground to make it stick.
Was this helpful?
M
Mark Sullivan
Lead writer · 8 yrs full-stack

Mark started coding in 2017 after switching from financial analysis. She's built production systems in Python (Django) and JavaScript (Node + React) at two startups, and has taught intro programming at his local community college since 2022. He owns the curriculum for variables, functions, conditionals, and loops on this site. More about Mark →