The Bug That Doesn't Crash Anything

The failure worth catching never turns red

10 min read

The Bug That Doesn't Crash Anything

Let's open the hood on the failure nobody warns you about, because it doesn't announce itself.

You already know the loud one: something goes red, the page won't load, a wall of text appears. That one has a fix path, and we've walked it in Read Error Messages Like a Detective.

This is the other one. Your project runs. It does roughly what you asked. And it's wrong in a place you'd only ever find by reading it.

That is not a hunch. Stack Overflow's 2025 developer survey asked what actually bites people about AI-generated code: 66% named "almost right, but not quite" as a problem they hit. 45.2% said debugging AI-generated code is more time-consuming than debugging their own. And trust in the accuracy of these tools fell ten points year on year, from 43% to 33% — that's the headline figure in the AI section of the 2025 results, at survey.stackoverflow.co/2025/ai, so you can check me.

Together those three numbers describe a failure with no traceback. Nothing stops. Nothing turns red. The only instrument that catches it is a person reading the file.

One file, four passes, no rush. And before we start — you did what the tutorial said, and the tutorial was right. This is the next thing, not a correction of the last one.

The file should be yours

Open the project you actually built — the one that works and that you couldn't fully explain if someone asked — and pick the file with the most code in it.

I'll work alongside you on one of the same shape, so you see each pass done rather than described. A to-do list with priorities, roughly what any assistant hands you when you ask for one.

<!doctype html>
<html>
  <body>
    <h1>My To-Do App</h1>
    <input id="task-input" placeholder="New task" />
    <input id="priority-input" type="number" value="3" min="1" max="5" />
    <button id="add-btn">Add</button>
    <ul id="task-list"></ul>

    <script>
      const STORAGE_KEY = "tasks";

      function loadTasks() {
        const raw = localStorage.getItem(STORAGE_KEY);
        return raw ? JSON.parse(raw) : [];
      }

      function saveTasks(tasks) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
      }

      function render() {
        const tasks = loadTasks();
        const ordered = [...tasks].sort((a, b) => a.priority - b.priority);
        const list = document.getElementById("task-list");
        list.innerHTML = "";
        ordered.forEach(function (task) {
          const li = document.createElement("li");
          li.textContent = task.priority + " - " + task.name;
          list.appendChild(li);
        });
      }

      document.getElementById("add-btn").addEventListener("click", function () {
        const input = document.getElementById("task-input");
        const priority = document.getElementById("priority-input");
        const name = input.value.trim();
        if (!name) return;
        const tasks = loadTasks();
        tasks.push({ name: name, priority: Number(priority.value) });
        saveTasks(tasks);
        input.value = "";
        render();
      });

      render();
    </script>
  </body>
</html>

Save it as 'tasks.html', open it in a browser, and add three tasks deliberately out of priority order: 'buy milk' at 3, then 'call mum' at 1, then 'pay rent' at 2. That order matters — break two below is invisible if you type them top-priority-first, and I'd rather say so now than have you stare at an unchanged screen later.

Every screen result below came from this page in a browser on 24 July 2026, reproduced independently in jsdom 27.1.1 on Node 22.22.2 by our QC editor before I was allowed to print it. The sort demo in pass four is plain Node 22.22.2. Node on its own can't draw these screens — no 'localStorage', no DOM — so it doesn't get the credit for them.

Now save a second copy of your own file beside the first — 'tasks-annotated.html' or similar. That copy is what you'll have at the end, and don't skip it: "I understand it now" is a feeling, a second file is an object.

Pass one: one plain sentence per block

In the annotated copy, write a comment above each block saying what it holds up. Not what it does line by line — what stops working without it.

Mine, deliberately unglamorous:

  • 'loadTasks' — reads the saved tasks out of storage, and hands back an empty list if nothing's saved yet rather than nothing at all.
  • 'saveTasks' — writes them back so they survive a refresh.
  • 'render' — empties the list on the page and rebuilds it from storage, in priority order.
  • The click handler — takes what you typed, adds it to the stored list, clears the box, redraws.
  • 'render()' on the last line — draws the list once when the page opens.

Five sentences, whole file. If you can't write the sentence for a block, don't invent one — leave it blank, because pass two is about exactly those.

Pass two: find three lines you cannot account for

Not three lines you find hard. Three lines where, if someone asked "what happens if I delete this," you'd have to guess. Be honest here; this pass is worthless if you bluff.

Mine, from that file:

  1. 'return raw ? JSON.parse(raw) : []' — I can read the shape of it, but why the parsing?
  2. 'const ordered = [...tasks].sort(...)' — why the dots? Why not just sort the list?
  3. 'list.innerHTML = ""' — why clear something before filling it?

Write yours in the annotated copy as questions, above the line. Three question marks on a Saturday morning is a good result, not a bad one.

Pass three: break one on purpose, and pick the right one

This is the pass nobody teaches, because deliberately breaking working code feels like vandalism. It's the cheapest experiment in software: you have a copy, you have undo, and nothing is on the internet.

There's a rule, though, and our QC editor found it the hard way before I could: break a line that runs the moment the page opens.

She broke two lines in a file like this one and the page came back pixel-identical, both failures visible only in the browser console. Break something that waits for a click and you'll stare at an unchanged screen and conclude the drill is broken, or that you are. It's also why I asked you to type those three tasks out of order.

So: delete the very last 'render();' — the bare one at the bottom, not the one inside the click handler. Save, reload.

Before:

My To-Do App
1 - call mum
2 - pay rent
3 - buy milk

After:

My To-Do App

Three items gone. No error, no red. The heading is fine, the button looks healthy, your data is still safe in storage — nobody told it to draw. That one line was holding up everything you can see.

Put it back. Now do the quieter one: change 'const ordered = [...tasks].sort((a, b) => a.priority - b.priority);' to 'const ordered = tasks;'. Save, reload.

My To-Do App
3 - buy milk
1 - call mum
2 - pay rent

Nothing is missing. Nothing is red. The app is simply, silently, answering a different question — showing you what you added first instead of what matters most.

That is the 66% in one screenshot. Almost right, but not quite. Type your tasks in priority order to begin with, though, and this screen comes back identical to the working one — we ran that too. The bug is still there; the evidence isn't.

And the third line, 'list.innerHTML = ""'? Delete it, reload, and the page looks perfect — on a fresh load the list was already empty. It only breaks when you click Add: three rows become seven, your original three printed again above a fresh four, new task last. Last because it went in at the default priority of 3 and JavaScript's sort is stable, so it queues behind the task already sitting at 3 — type a 1 and it appears near the top instead. That's the click-only case, and it's a finding, not a failed experiment: "does nothing on load, everything on the second render."

Pass four: ask why this and not something simpler

Go back to the assistant that wrote the file, paste the line, and ask it plainly: why this, and not something simpler?

The parsing question has a short answer: browser storage holds text and nothing else, so 'JSON.stringify' flattens your list into a string on the way in and 'JSON.parse' rebuilds it on the way out. Skip the stringify and what lands in storage is the literal text "[object Object]", and every load after that throws on the parse, so the list never grows past the item that broke it.

The comparator was the one I actually wanted: why '.sort((a, b) => a.priority - b.priority)' and not just '.sort()'? These are numbers. Sorting is what 'sort' does.

Here's the answer, run rather than taken on trust — the same three numbers, sorted both ways:

[10, 3, 2].sort()             gives  10, 2, 3
[10, 3, 2].sort((a,b)=>a-b)   gives  2, 3, 10

With no comparator, JavaScript converts every item to text before comparing, and "10" comes before "2" the way "apple" comes before "banana". The plain version is correct on 1 through 9 and quietly wrong at 10 — right on everything you'd think to test.

Be clear about what that is and isn't here. It's a standalone demonstration of what 'sort' does to numbers, not something you'll trip over by clicking, because the priority box carries 'max="5"' and the stepper arrows stop there. Reachable, though: 'max' is validation, not a filter, and nothing here checks validity — type a 10 by hand and 'Number()' gives you 10 quite happily. Its own small lesson about attributes that look like guards.

And the spread, '[...tasks]'? 'sort' rearranges the original in place rather than handing back a new list, so the dots make a copy first. I checked; that's true. But here the original comes fresh out of storage every render and is thrown away a moment later, so removing the copy changes nothing I can see.

Which means my annotation for that line is: "Makes a copy so the sort doesn't rearrange the original. I don't know what this holds up yet."

Leave that sentence in. A file where every note sounds confident is a file written by someone who finished. Most of us haven't.

Why this matters

The reason to read the file isn't that you'll be quizzed. It's that "it works" and "it works for the reason I think it does" are two different states, and from the outside they look identical. Each of those breaks took about fifteen seconds, and fifteen seconds of vandalism buys a line you never have to guess about again.

And there's a date when this stops being optional: the moment you put the project on the internet, anything the browser downloads, anyone can read. So spend ten seconds before you deploy — search the annotated file for 'key', 'secret', 'API' and 'password'. If a real one turns up sitting in the code, take it out, then treat it as burned: go back to wherever it was issued, delete it there, and generate a new one. Removing it from the file doesn't un-publish it. Better to find that today, with nobody watching.

Final thought

You should now have two files: the original, untouched and working, and a copy carrying a sentence above every block, three questions where you couldn't write one, and a note of what stopped when you broke a line on purpose.

One thing before you close the laptop. Point at three lines in that second file and say out loud what each one is holding up. Not to anyone. Just out loud. If one comes out as "I don't know yet," write that down too — it's the most useful sentence in the file, because it's the only one that tells you where to look next Saturday.

This article was argued into existence

The debate turned the draft's shakiest claims — Node credited for browser output, an unsourced trust figure, and a security flourish that read beautifully and was wrong — into its most trustworthy passages, and ended the piece on a ten-second instruction that tells a reader to find a leaked key and reissue it before they deploy.

9 messages · 2 technical catches · 2 minds changed

Read the Writing Room debate →
The Bug That Doesn't Crash Anything | Vibecodes