Why one small change broke something you never touched

The bug isn’t in the diff you’re staring at — it’s in the file you never opened

5 min read

Argued into existence in the Writing Room7 messages · 1 mind changed
Why one small change broke something you never touched

Somewhere in the last week you asked your agent to touch one function — clean it up, add a field, wrap it in an object. It handed back a clean diff. Then somewhere else, a screen you didn't even open, something quietly stopped working. No red error. No stack trace pointing back at the change. Just a value that used to be there and now isn't.

I want to show you exactly where that bug lives, because it isn't where you're looking.

The diff you're staring at is innocent

Here's the trap: when something breaks right after an edit, the instinct is to reread the file the agent just touched. Nine times out of ten you don't find one, because the file the agent touched is fine. The bug is somewhere else entirely, in a file with zero lines changed, that never shows up in any diff, that nobody thought to open.

That's not bad luck — it's a specific, repeatable mechanism, and naming it is what makes it stop feeling random.

What a function's "shape" means

Every function makes a promise about what it hands back, whether or not anyone wrote that promise down. A function called saveUser that returns {id: 501, email: 'sam@example.com'} is promising: call me, and you get an object with an id on it. Every piece of code downstream that calls saveUser and reads .id is leaning on that promise.

Ask an agent to "clean up" that function, and one of the most reasonable-looking changes it can make is to wrap the return value — bundle the row with some metadata instead of returning the row directly. It's tidier. It also breaks the promise, and nothing in the language stops it, because plain JavaScript checks nothing about shapes at runtime. Reading a property that isn't there anymore doesn't throw — it just hands back 'undefined' and moves on.

Watching it happen

Here's saveUser.js before anyone touches it, next to auth.js — the file that calls it and, in a real project, is the login logic nobody reopens once it works:

// saveUser.js
function saveUser(email, password) {
  // pretend this is a database insert that returns the new row
  const row = { id: 501, email };
  return row;
}
module.exports = { saveUser };
// auth.js
const { saveUser } = require('./saveUser');

function createAccount(session, email, password) {
  const user = saveUser(email, password);
  session.userId = user.id;
}

function requireLogin(session) {
  if (!session.userId) {
    return { redirect: '/login' };
  }
  return { ok: true, userId: session.userId };
}

module.exports = { createAccount, requireLogin };

Run a signup through it (this is real output, Node 22.22.2):

session after signup: { userId: 501 }
requireLogin result: { ok: true, userId: 501 }

Now I ask an agent to "clean up saveUser so it also returns some metadata about the save" — a small, scoped, entirely reasonable request:

// saveUser.js — after the edit
function saveUser(email, password) {
  // "tidied" this to include metadata alongside the row
  const row = { id: 501, email };
  return { data: row, meta: { source: 'db', savedAt: Date.now() } };
}
module.exports = { saveUser };

I have not touched auth.js. Not one character. Diff it against the original:

$ diff before/auth.js after/auth.js
$

Empty. Zero lines changed. Run the exact same signup through that exact same untouched file:

session after signup: { userId: undefined }
requireLogin result: { redirect: '/login' }

Nothing threw. No stack trace, no red text, nothing pointing back at saveUser.js. The person signing up just gets bounced to the login page, for a reason that looks, from where they're sitting, like nothing at all.

The diagram

BEFORE THE EDIT

  saveUser.js                     auth.js
  -----------                     -------
  return row                --->  const user = saveUser(...)
  { id: 501, email }              session.userId = user.id   -->  501


AFTER THE EDIT  (only saveUser.js was touched)

  saveUser.js                     auth.js            <- zero lines changed
  -----------                     -------
  return { data: row, meta }--->  const user = saveUser(...)
                                   session.userId = user.id   -->  undefined
                                                                       |
                                                        (nothing throws here)
                                                                       v
                                   requireLogin(session)  -->  redirect '/login'

That empty stretch in the middle is the whole article. There's no error there because there's nothing to error on. auth.js asks user.id for a property that no longer exists, and JavaScript hands back undefined like that was a perfectly normal thing to ask for — because to the language, it was.

The one habit that shrinks this

You can't manually trace every caller of every function before every edit — that's not a habit, that's a full-time job. What actually works is one sentence added to the request that's about to touch a function other files depend on:

"Add the save metadata, but don't change what saveUser returns."

That sentence turns an invisible assumption into a constraint the agent has to work around instead of quietly overwrite. If it still needs somewhere to put the metadata, it now has to find another way — a second argument, a separate function, a wrapper at the one call site — the line in auth.js asking for it — instead of reshaping the object every other caller already trusts.

Pair that with the other half of the habit: commit the working version before you ask for the change. Not because git catches the bug on its own — it won't — but because "working" and "broken," one commit apart, is what makes git diff mean something when a screen you didn't touch starts acting strange. Without it, you're guessing which of the last six changes did it.

Why this bites more now than it used to

When you wrote every line by hand, you were the one keeping shapes straight in your head — you'd never forget that saveUser returns a row. The agent-first workflow most of us are on now — describe the feature, read one diff, move on to the next request — routinely touches a function without you ever holding, at the same time, the list of every other file quietly depending on its old return value. That's not a knock on the tools. It's just what changes when the person writing the function and the person tracking its callers stop being the same attention span.

Why this matters

This isn't really about saveUser. It's about the fact that even a small early project is a mesh of files quietly agreeing with each other about what shapes to expect, and an agent that rewrites a function body doesn't automatically know who downstream is counting on the old one. A typed language catches a version of this before you ever run the code. Plain JavaScript or Python, which is what most first projects are written in, has no compiler — no step that catches a shape mismatch before runtime — so the check has to come from you, in the sentence you type before the code arrives, not in a review after it's already shipped.

Final thought

Next time you ask an agent to touch a function that anything else in your project calls, add the one sentence before you hit enter: name what must not change. Commit first, so that when something breaks on a screen you didn't open, you have a clean, known-good line to diff against — instead of a guess.

Why one small change broke something you never touched | Vibecodes