The Rename That Deleted a Feature Without Breaking Anything

The Rename That Deleted a Feature Without Breaking Anything

The Rename That Deleted a Feature Without Breaking Anything

For twelve days, the "contact us" link on my checkout error screen did not exist.

The page rendered. The error rendered. Nothing threw, nothing logged, no test went red. The only symptom was an absence, and absences do not page you at 2am.

I found it by accident, reading the page source for something else entirely.

What the code was doing

The checkout page turned an error message into a clickable mailto link whenever the message named our support address. Roughly this:

if (/[email protected]/.test(msg)) {
  var a = document.createElement('a');
  a.href = 'mailto:[email protected]?subject=' + encodeURIComponent(...);
  ...
}

Two lines, one address, two completely different jobs. The first line tests for the address. The second line offers it.

Weeks earlier I had moved that address off everything a customer sees. It was an internal mailbox and it had no business being in a checkout flow. So I changed the mailto: on the second line to the public address and moved on.

The regex on the first line stayed exactly as it was.

Why nothing complained

From that commit onward, the condition was true of no message the application produces. The branch stopped running. Not "ran and failed" but "never evaluated to true again."

There is no error state for that. A function that throws gets a stack trace. A function that is never called gets silence. Every automated check I had was looking for things that go wrong, and nothing had gone wrong. Something had simply stopped happening.

The tests passed because the tests covered the error rendering, which still worked perfectly. The page still showed the customer what had failed. It just no longer gave them a way to reach a human about it.

That is the part that stings. The errors this branch existed for are the ones a buyer hits after choosing a four-figure plan. The single worst moment to remove someone's route to support is the moment they are trying to give you money.

The edit that felt finished

Here is what I want to be honest about: the change I made looked complete while I was making it.

Both lines were in the same diff, four lines apart. I read the diff. I saw the address I had just corrected and my brain registered "that address is now right." The output was visibly correct. The producer was fixed.

The matcher, sitting directly above it, was doing the opposite job with the same string, and my eye slid straight over it.

This is a specific and repeatable failure. When a value appears twice in one function, once to generate output and once to test for it, fixing the generator feels like finishing. The result you can see is now correct. The condition you cannot see quietly stops matching.

Updating the code that produces a value does not update the code that recognizes it.

Grep for the value, not the intent

The habit I changed is small and I now apply it every time.

When I change a value, I search the project for the value. Not for the feature, not for the function name, not for the thing I set out to edit. The literal string, before I commit.

rg "[email protected]"

If that returns anything after I believe I am done, I am not done. It takes three seconds and it would have caught this in the same minute it was created.

This matters more with an AI assistant in the loop, not less. Ask a model to "change the support address on the checkout page" and it will do exactly that, well, and report success. It fixes what the instruction named. It does not, unprompted, go looking for the same string wearing a different hat two lines up. So I now say the quiet part in the prompt:

Change this address everywhere it appears in the project, including inside conditionals, regular expressions and tests. List every file and line you changed.

The word "including" is doing real work there. Without it, you get the obvious occurrences and a confident summary.

Make the two agree, in one line

The stronger fix is not a habit at all, it is an assertion.

The bug here was a disagreement between two constants that must always match: the address the code looks for, and the address the code offers. That is testable, and the test is one line:

assert(SUPPORT_PATTERN.test(SUPPORT_ADDRESS));

Better still, derive one from the other so they cannot drift. A single constant, a pattern built from it, and the disagreement becomes impossible rather than merely detectable.

I have started looking for this shape deliberately. Any time a conditional tests for something another part of the system emits, that pairing is a silent failure waiting to happen. Feature flags matched by name. Error codes tested as strings. Routes checked against a prefix somewhere else. All the same trap.

The real lesson

Broken code announces itself. Deleted behavior does not.

When you change a value, the risk is not that something crashes. The risk is that a condition somewhere stops being true, a branch stops running, and your application carries on looking healthy while doing slightly less than it used to.

Search for the value. Make the matcher and the producer agree. And when an assistant tells you a rename is done, ask it what it left behind.