← All posts

The controls we deleted, and why a switch that does nothing is worse than a missing feature

There is a failure mode in software that does not show up in any test suite, because nothing fails. Nothing errors. Nothing crashes. A control is drawn, a member changes it, the value is saved, and no code anywhere reads it.

We have shipped this more than once. This post is the inventory, because I think the pattern is common enough in this category that naming it is useful, and because the fixes were mostly deletions.

1. Two notification dropdowns that gated nothing

Our notification preferences page used to carry two frequency selects — one for upcoming events, one for new content — alongside the switches.

They saved correctly. They loaded correctly. They were rendered from the design of record. And an audit of what actually reads those keys found the answer: nothing did.

Which means a member who opened their settings, found "Upcoming events", set it to Never, and closed the page, kept receiving event reminders. Not because of a bug in the sending logic. Because the sending logic had never heard of the setting. From the member's side this is indistinguishable from us ignoring them on purpose.

We removed both controls rather than wiring them up, and that choice is the interesting part. The obvious fix is to convert a five-value cadence into a boolean and gate on it. But somebody who set "Weekly" meant something specific, and silently reinterpreting their saved value as on or off is a second decision made on their behalf without telling them. We deleted the controls, left the stored values untouched so nothing was destroyed, and left the switches that do work.

Related, and worse: the Likes switch on that same page had always been drawn, and had no key behind it either — reactions were ungated until we fixed it. At the time, reaction notifications were the majority of all notifications in production. The single noisiest thing in the product had a visible off switch that did nothing.

2. Three moderator permissions that unlocked doors into empty rooms

Our moderator role is scoped: you grant a moderator specific channels and courses, plus a set of capabilities. That list used to include three full-access grants — manage the broadcast schedule, manage workflows, manage landing pages.

Each of them widened access to one parent table. Every sibling table, and all five of the operations you would actually perform, stayed admin-only. So the practical effect of granting a moderator "manage workflows" was: a link appeared, they clicked it, and the page refused them.

That is worse than not having the permission, because now somebody believes they can help, tells the host they will handle it, and discovers on Thursday that they cannot.

We removed all three. Then we did the thing I would recommend to anyone making this class of fix: we narrowed the type the permission draft is built from, down to exactly the two keys that are genuinely enforced. Re-adding one of those checkboxes is now a compile error rather than a plausible-looking commit. A deletion that a future person can casually undo is a deletion with a half-life.

There are four more capability names that exist in our catalogue and are deliberately not offered in the interface, for the same reason: nothing enforces them yet.

3. Four "Preview" buttons that could not work

Our onboarding admin has tabs for the welcome screen, the introduction gate and the default landing screens. Each of them configures something a new member sees.

The natural thing to add is a Preview button. We wrote a test that asserts those buttons do not exist.

The reason is that owners and admins are exempt from every onboarding gate — deliberately, because the alternative is locking the person who runs the community out of their own admin panel over their own configuration. That exemption is evaluated before the audience check runs. So an admin's own session can never reach the state being previewed. A Preview button there would either show them something fabricated or show them nothing, and either way it would be answering a question it cannot answer.

Writing a test to keep a button from existing feels absurd until you have watched the same well-meaning button get re-added twice.

4. Drag-and-drop, everywhere

Not a bug, just a decision, and the most-questioned one on this list.

There is no drag-and-drop reordering anywhere in Klaas. Channels, course sections, lessons, pricing options, pathway courses — every ordered list is Move up / Move down. Our own design mockups showed drag handles. The implementation refused them.

Drag-and-drop is better on a large screen with a mouse and a short list. It is worse on a phone, worse with a trackpad, worse with fifty items, worse with a screen reader, and much worse when the save fails halfway. Move up / move down is unglamorous, works identically on every device and input method, and never leaves you unsure whether the thing you dragged actually landed.

If we ever add drag-and-drop it will be in addition to the buttons, not instead of them.

The rule we adopted

Never offer a control that cannot work.

Which sounds too obvious to be worth stating, until you notice how controls come to exist. A design specifies a screen. The screen has a switch on it. The switch gets built, because building it is easy and it matches the design. The thing behind the switch is harder, so it gets deferred. The switch ships.

Nobody lied. Nobody was careless. There is just no moment in that sequence where anyone is responsible for checking that the switch reaches something.

Three practices that actually catch it, all of which we adopted after being caught:

Audit from the reading side. Do not ask "is this setting saved?" — it always is. Ask "what code reads this key?", and answer it with a search, not from memory. Every one of the defects above was found that way and none of them was found by a test.

Make the absence structural. Delete the control, then narrow the type so re-adding it does not compile. Tests can be deleted. Type errors have to be argued with.

Give the interface the honest answer. Where a capability genuinely does not exist yet, say so in words instead of drawing a hopeful button. Our integrations page does this: a connector that needs a platform-level OAuth app we have not built shows the actual refusal — "requires OAuth setup by the platform operator" — passed straight through from the server rather than dressed up. It looks less finished. It is more finished, because "less finished" is the true state and the interface is now reporting it.

Why write this down

Because a switch that does nothing is a specific kind of dishonesty, even when it is entirely accidental. The member changed a setting. They believe something is now different. The gap between what the interface promised and what the software does is being paid for by them, in unwanted emails, or in a moderator who cannot moderate.

We would rather have a shorter settings page that is completely true.