Nobody files a bug called “our email comparison is inconsistent.”
What lands in your tracker is: the unsubscribe did not work. Or: this customer has two support threads. Or: my invitation link says it is not for me.
Those are three different bug reports, from three different parts of a product, and they have one cause.
Four capabilities compare addresses
auth matches a sign-in address. email checks one against the suppression list. support links an incoming message to an account by its From header. testers mints an invitation against one.
Four places deciding whether two strings are the same person. Four chances to decide differently — and you will not find out which one disagreed, because none of the symptoms point at the comparison.
And when they disagree, the symptom never mentions addresses:
- An invitation is minted against
Ada@example.com, every session carriesada@example.com, and the invitee clicks a link that does not recognize them. - A suppression is stored one way and checked another, so someone who unsubscribed keeps receiving mail. That is not a UX complaint — that is a deliverability problem and, depending on where they live, a legal one.
- One customer arrives as two threads because
Fromwas normalized differently from the account address.
So there is one rule, in core, and all four use it. Not because address handling is interesting, but because a disagreement between four implementations is invisible until it is expensive.
Every clause of semver §11.4 is a trap
The other one looks even smaller. Versions are strings with dots in them. Sorting them is a solved problem.
Anything that has to rank versions needs the precedence rules from the semver spec — ordering a release feed, deciding what sits between installed and latest, sorting a prerelease against the stable release it precedes.
Those rules are short, and each clause is a separate way to be wrong:
- Numeric identifiers compare numerically; alphanumeric ones compare lexically.
- A numeric identifier ranks below an alphanumeric one.
- When every shared identifier is equal, the longer identifier set wins.
- A stable release outranks every prerelease of the same core version.
Each of those is one line to get wrong. And getting one wrong does not throw — it produces a feed in a slightly odd order, which nobody investigates because it looks like a display quirk rather than a comparison bug.
So: one parse, one format, one order, written once.
Why these are worth a post
Because your instinct with both is that they are too small to centralize. An email comparison is a toLowerCase(). A version sort is a localeCompare. Pulling them into a shared module feels like ceremony around something trivial, and you have real work to do.
The instinct is wrong, and the reason is not that the code is hard. It is that these are primitives whose bugs do not look like bugs in them.
A broken date formatter shows you a wrong date. You look at the date code. But a broken address comparison shows you a support ticket about unsubscribes, and a broken version sort shows you a release feed that seems fine. The distance between the defect and the symptom is what makes duplication expensive here, far more than the difficulty of the code.
The test worth applying
Before you copy a small helper rather than share it, ask what its failure would look like to you from the outside.
If a subtle disagreement would surface as a clearly wrong output in the same place — a mangled string, a bad date — copying it is survivable. Somebody will see it and fix it.
If a subtle disagreement would surface somewhere else entirely, as a business problem in a different subsystem, then the copies are not a maintenance cost. They are four independent chances to create a bug that nobody will trace back.
Address comparison and version ordering are both firmly in the second category, which is why two of the least interesting modules in the kit are also two of the ones most worth having exactly once.