When Things Go Wrong

Debugging Is a Conversation Too

Your code will have errors. Not might have. Will have. Everyone’s code has errors. The programmer who wrote JavaScript itself wrote code with errors. The team that builds your web browser shipped errors. Every piece of software you’ve ever used contains bugs that nobody’s found yet, lurking like undiscovered species in the computational undergrowth.

This is not a moral failing. It is not a sign you’re unsuited for the craft. It is simply the nature of communicating complex instructions to an entity of spectacular, almost heroic literalness, one that will do exactly, precisely, relentlessly what you said instead of what you meant. The computer, unlike a helpful colleague, will not think “surely they meant the other thing” and quietly fix your mistake. It will march confidently off the cliff you accidentally pointed toward.

So what actually separates a beginner from an experienced programmer? Not bug-free code, sadly. The experienced one has simply developed a systematic approach to finding and fixing the bugs that turn up regardless. They’ve made their peace with being wrong, frequently and spectacularly, because they know how to recover.

This is also where ordering quietly stops being enough. You can ask for a working program the way you’d name a dish and let the kitchen handle it, and often you’ll get one. But when the sauce splits, no amount of asking nicely tells you why; you have to understand what’s actually happening in the pan, and fixing it is a cook’s skill, not a diner’s.

Three Kinds of Miscommunication

Not all errors are created equal. Understanding what kind of creature you’re facing helps immensely when figuring out how to dispatch it.

Syntax Errors: Grammatical Misadventures

Syntax errors are like missing the closing quote in a sentence. JavaScript, like a fastidious grammarian confronted with “I apple want the,” can’t even begin to parse your intention. The grammar is broken. The sentence structure has failed in some fundamental way.

Paradoxically, these are the friendliest errors because:

  • JavaScript catches them before running any code at all
  • The error message points directly at the scene of the crime
  • They are usually obvious once you actually look

A missing closing brace, an unmatched paren, a stray comma where none belongs: each violates JavaScript’s expectations about proper grammatical structure. JavaScript, unlike a polite dinner guest, tells you immediately.

Syntax errors feel embarrassing when you’re starting out. You’ll spend ten minutes hunting for the bug, only to discover you typed consol.log instead of console.log. This is normal. Everyone does this. Professional programmers do this daily. The embarrassment fades. The typos, alas, do not.

Runtime Errors: When Reality Objects

Some code is syntactically perfect (JavaScript understands it completely) but when it actually tries to execute, reality raises an objection. Reaching into null for a property that isn’t there, for instance. JavaScript has no patience for null.name; it stops cold with a TypeError. The program stops the way a car stops when the road does.

These are runtime errors. The code is written correctly, but you asked for something impossible. The file doesn’t exist. The value isn’t the shape you assumed. The function you’re calling doesn’t actually exist. JavaScript started running your program with admirable enthusiasm, encountered something it can’t do, and stopped in bewilderment.

Runtime errors are trickier than syntax errors because:

  • The code looks perfectly respectable
  • The error only appears when that particular line executes
  • The problem might depend on external factors entirely outside your control: missing files, network outages, or users typing things that no reasonable person would ever type (and yet, they do)

Logic Errors: The Silent Betrayal

And here’s the sneakiest creature in our menagerie. There’s no error message. JavaScript runs the code with evident satisfaction. But the answer is quietly, insidiously wrong.

The culprit might be operator precedence, where division happens before addition and calculates a + (b / 2) instead of (a + b) / 2. JavaScript did exactly what you said. You simply said the wrong thing. The computer followed your instructions to the letter, and therein lies the tragedy.

Logic errors are the hardest to find because:

  • There’s no error message to guide you (none whatsoever)
  • The program appears to work perfectly
  • You have to somehow notice that the output is wrong
  • You then have to trace through the logic to find where your thinking diverged from reality

This is where testing becomes crucial. If you don’t verify that your average function returns the expected value, you might never notice the bug lurking in your code, waiting patiently to embarrass you at the worst possible moment.

Classify code snippets by error type. Like a naturalist identifying species, you’ll learn to recognize these creatures in the wild.

Reading Error Messages: The Computer Is Trying to Help

Error messages look intimidating, especially the ones that scroll across multiple screens like the closing credits of a film nobody enjoyed. But they’re actually trying to help you. The computer, in its own awkward and verbose way, is attempting to explain what went wrong.

Click each part of this error message to understand what information it provides.

A typical error message contains several key pieces: the stack trace (announcing the sequence of events leading to the catastrophe), the file location and line number, the actual line of code that caused the trouble, the species of error, and the specific complaint. Armed with all that, you can walk straight to the problem and work out what went wrong.

A Field Guide to Common Error Species

ReferenceError: You used a variable that doesn’t exist. Often a typo.

TypeError: You asked incompatible types to cooperate, or tried to call something that isn’t actually a function. They declined.

The Silent undefined: You reached for an array position or object property that doesn’t exist, like asking for the fifteenth egg from a carton of twelve, or asking a filing cabinet for a folder it doesn’t hold. JavaScript won’t stop you. It just hands back undefined and lets you carry on, often straight into a crash three lines later, once that undefined collides with code that expected a real value.

NaN: You asked JavaScript to do arithmetic on something that isn’t quite a number. "25 minutes" * 2 doesn’t throw; it quietly becomes NaN, and NaN has an unsettling habit of infecting every calculation it touches afterward.

SyntaxError: A grammatical transgression. The code cannot even be parsed.

Learn to recognize these species by their markings, and you’ll often know roughly what went wrong before you’ve even looked at the code.

A field guide to the common error species you’ll encounter.

Debugging: The Art of the Investigation

Debugging is the art of figuring out where the miscommunication happened. It’s detective work: gathering clues, forming hypotheses, testing them systematically, and occasionally staring at the screen in quiet disbelief that such a simple mistake could have caused such chaos.

Strategy 1: Actually Read the Error Message

This seems obvious, and yet many beginners panic and skip past the error message entirely in their haste to change something, anything. Resist this impulse. Start with what the computer is telling you.

  • What line is the error on?
  • What species of error is it?
  • What does the description say?

Often, that’s enough. “ReferenceError: colour is not defined” tells you the problem with admirable clarity: you used colour but never defined it. Maybe you meant color, or maybe you just forgot to assign a value.

Strategy 2: The Noble Art of Print Debugging

When the error message isn’t enough, or when there’s no error but the output is stubbornly wrong, scatter console.log statements through your code like breadcrumbs. Now you get to watch what’s actually happening inside a function, like switching on the kitchen light to see what the cat knocked over, instead of guessing in the dark. Did it receive what you expected? Is the loop running the right number of times? Are the values what you assume they are?

Print debugging is unsophisticated. It is also devastatingly effective. Professional programmers use it constantly, though they may not admit it at parties.

You’ll hear about “proper debuggers” that let you step through code line by line, inspect variables, and set breakpoints. These are useful tools. But console.log statements are faster for quick investigations, work everywhere (including production servers at 3 AM), and require zero setup. Use both. Never feel ashamed of adding console.log("got here") to verify that a line is even executing.

Step through code with print statements to see how values change.

Strategy 3: Interrogate Your Assumptions

“I am certain this variable is a number.” Is it?

“This array definitely has items in it.” Does it?

“The user would never type that.” They absolutely will.

Many bugs hide behind assumptions that seem obvious but are quietly, devastatingly wrong. Don’t assume. Verify.

Every bug hides behind an assumption. Click each to see how to verify it.

Strategy 4: Simplify Ruthlessly

If you can’t find the bug in a 50-line function, remove code until it works, then add it back piece by piece. Brutal but effective. This technique is called “binary search debugging” when done systematically. Find the minimum code that reproduces the bug, and you have found the bug.

Using binary search, you can find any bug in log2(n) steps. Click lines to add checkpoints.

Strategy 5: Walk Away

Sometimes you’re just too close to see the problem. You’ve stared at the code for an hour. Your eyes glaze over. You’re convinced it should work, and yet it stubbornly doesn’t.

Step away. Get a cup of tea. Take a walk. Come back in ten minutes.

Remarkably often, you’ll see the bug the moment you sit back down, the way a name you couldn’t remember arrives just as you stop chasing it. Fresh eyes work miracles that exhausted ones can’t.

Strategy 6: Explain It to Something (Even a Duck)

There’s a technique called “rubber duck debugging,” named after a story in The Pragmatic Programmer. A programmer would carry around a rubber duck and debug code by explaining it, line by line, to the duck.

“Okay, duck, this function is supposed to calculate the average. First, I add all the numbers together. Then I divide by… wait. I’m dividing by 2, not by the number of items. That’s the bug.”

The act of explaining forces you to articulate what you think the code does, which often reveals the gap between what it actually does and what you intended.

This works because of a quirk of human cognition: when you read your own code silently, your brain helpfully fills in what you meant to write. But when you explain it aloud, even to an inanimate object, you’re forced to process what’s actually there. The words coming out of your mouth don’t match the intention in your head, and suddenly the error becomes obvious. The duck, it must be said, never judges.

Explain your code to the duck. The act of explanation often reveals the bug.

Debugging AI-Generated Code

AI-generated code has the same three error types, but the distribution shifts. AI rarely makes syntax errors: it knows JavaScript’s grammar perfectly. Runtime errors appear occasionally, usually from assumptions about data that didn’t hold. But logic errors? AI loves logic errors. It will confidently generate code that runs beautifully and does precisely the wrong thing.

This happens because AI optimizes for plausibility, not correctness. The code looks right. The structure is sensible. The variable names are appropriate. It has the easy confidence of a tour guide who has never actually been to the city. And yet, somewhere in the logic, there’s a quiet misunderstanding about what you actually wanted.

The debugging conversation loop with AI is surprisingly effective:

  1. Copy the error message (or the incorrect output)
  2. Paste it back to the AI
  3. Ask “what went wrong?”

AI is often remarkably good at diagnosing errors in its own code. It will spot the type mismatch it introduced, recognize the off-by-one error in the loop, notice that it’s checking the wrong condition. Sometimes the fix is immediate and correct.

But here’s when to investigate yourself rather than trusting AI’s fix: if the proposed solution is a band-aid (wrapping everything in try/catch to hide the error, or quietly papering over an undefined instead of asking where it came from) that’s a sign the real problem is elsewhere. Band-aids suppress symptoms. They don’t fix root causes.

Look for fixes that address the logic, not ones that route around the error. If AI suggests const result = data[0] ?? fallback;, ask why data might be empty in the first place. The real fix is probably further upstream.

Testing becomes more important with AI code, not less. AI can generate plausible-looking code that passes casual inspection but fails edge cases in creative ways. It might handle normal lists but crash on empty ones. Work correctly for positive numbers but return nonsense for negative. Process ASCII perfectly while mangling Unicode.

Write tests. Run them. Trust the tests, not the vibes. The code might look fine. It might seem reasonable. But until you’ve verified that it produces the correct output for the inputs you care about, including the weird ones, you don’t actually know if it works.

See how assertions catch bugs automatically by comparing working and buggy functions.

Testing: The Formalization of Distrust

Testing is how you verify that what you built does what you meant it to do. It’s formalized assumption-checking: the systematic practice of not trusting yourself.

Simple Testing

Run the code with known inputs and verify that the output matches expectations. Deceptively simple. Enormously powerful. You can make this concise with console.assert(): if the condition is false, the console logs an error and a stack trace pointing at the failure. Unlike a stricter language’s assertions, your program keeps right on running afterward, so treat a failed assertion as a note in the margin, not a full stop, and go check it before moving on. If nothing gets logged, you’ve verified (at least for these specific cases) that the function behaves as intended.

Testing Edge Cases

Don’t just test the happy path. Test the weird inputs, the boundary conditions, the cases that seem unlikely but will inevitably occur. Edge cases are where bugs love to hide, in the corners nobody sweeps. What if the array is empty? What if the number is negative? What if the string is nothing but spaces? Test these explicitly, because they will find you eventually.

Identify which edge cases are critical to test for an average function.

Why Test?

  • Catch bugs before users do (users have long memories for bugs)
  • Confidence when making changes (run tests, see immediately if you broke something)
  • Documentation of expected behavior (tests show how the code should work)

Testing feels like extra work when you’re starting out. But the first time you modify a function and immediately discover via tests that you broke something, before you committed, before you deployed, before a user filed an angry ticket, you’ll understand the value.

A Fuller Taxonomy of Errors

We’ve covered the primary species (syntax, runtime, and logic), but the ecosystem of errors is richer and stranger than this simple classification suggests.

Off-by-One Errors: The Fencepost Problem

Off-by-one errors are so common they have their own folklore. The classic formulation: if you’re building a fence with 10 sections, how many posts do you need? The answer is 11. One at the start of each section, plus one at the very end. This seems obvious when stated, yet programmers forget it constantly.

Arrays start at 0, not 1. Ranges are inclusive at the start, exclusive at the end. Array indices run from 0 to length-1, not 1 to length. Miss any of these by one, and your code is almost right, which perversely makes it harder to debug than code that is completely wrong.

Off-by-one errors are infamous. Can you spot them?

Race Conditions and Other Beasts

Race conditions, where a bug shows up or politely vanishes depending on which piece of code wins a footrace it never knew it had entered, get their own treatment when we discuss concurrency.

Learn to read stack traces: bottom-up for “what broke?” and top-down for “how did we get here?”

Some bugs have achieved genuine infamy. See the appendix on Famous Bugs for cautionary tales that have entered programming folklore.

Errors Are Normal

Errors aren’t failures. They’re feedback. They’re the computer telling you something went wrong, which is infinitely preferable to silently doing the wrong thing while maintaining an air of smug confidence.

A program with no errors that produces wrong results is far worse than a program that crashes with a helpful error message. At least with the error, you know where to look. The silent bug is the dangerous one, the slow leak under the sink rather than the burst pipe.

Every programmer deals with errors daily, from the absolute beginner to the architects of operating systems. The difference is experience: knowing which strategies to apply, recognizing error patterns by their markings, having mental models for where bugs typically make their homes.

You’re building that experience right now. Every bug you find, every error you fix, every time you track down a logic error by scattering console.log statements through your code, that’s you getting better at what may be the most important skill in programming: the art of debugging.

We tend to treat debugging as the thing that gets in the way of the real work. It’s the other way around. Writing the code was never the hard part. Finding out why it doesn’t yet do what you meant: that was always the job.

A gentle reminder for when debugging gets frustrating.

What Lies Ahead

So far, we’ve been building programs from scratch, writing every function ourselves, explaining every concept from first principles. You don’t have to.

Other programmers have already solved a staggering number of problems, then left the solutions lying around for you to pick up. Functions for working with dates, sending emails, processing images: written, tested, and waiting. In the next chapter, we’ll explore how to use code written by others, and how to ask AI to help you find the right tools.


Next: Programming isn’t just about writing code from scratch. It’s about building on the work of others.