A language almost exactly like yours, except it doesn’t like keeping secrets
Every conditional contains at least two outcomes. Writing down only one does not eliminate the other. It merely delegates its interpretation to the next person.
Usually that person is you, six months later, during an incident.
Wilson’s Law of Branching:
Every branch has an alternative. An alternative that is not expressed becomes a secret, and every secret eventually acquires a maintainer.
This is not a mathematical law. Mathematical laws have proofs. This one has deployment history.
The original Wilson Else proposed making certain missing alternatives visible, particularly inside nested conditionals. But requiring an else everywhere would confuse acknowledgment with punctuation. Prohibiting else everywhere would confuse flat code with complete reasoning.
Perhaps the problem is not whether we wrote else. Perhaps the problem is whether we told the truth about the decision. The original Wilson Else classified the original problems as "lies" versus "truths" but this essay attempts to soften the message as "secrets" vs. "truths". Secret is still a pejorative word but it is less pejorative than lies.
To investigate, we will invent a language almost indistinguishable from a normal programming language. Its distinguishing feature is that it gets uncomfortable when you appear to know something you have not told it.
1. First, declare what kind of decision this is
Ordinary languages let one keyword perform several jobs.
An if might mean:
- Do this only when appropriate.
- Reject this input and stop.
- Choose between two outcomes.
- Start a priority-ordered sequence of alternatives.
- Conceal a question nobody answered.
Our language distinguishes three forms:
| Construct | Meaning |
|---|---|
if | Perform a conditional action, or terminate through a guard. No else. |
realisedIf | Make a binary decision with two nonempty executable branches. |
wilsonIf | Perform an action or explicitly acknowledge that nothing happens. |
This does not make the program correct. It makes certain kinds of evasiveness syntactically inconvenient.
realisedIf (condition) { positiveAction() } // SYNTAX ERROR: unrealised-else // Provide an alternative, or declare a conditional action. The author has announced a binary decision and delivered half of it.
We could automatically insert:
else { // TODO: nothing } But that would manufacture evidence that a decision had been acknowledged when, in fact, a tool had inserted some braces.
We have promoted an omission into a compliance artifact.
Instead, the author must choose: supply the alternative, use an ordinary conditional action where permitted, or explicitly declare inaction.
2. Nothing is an outcome, not an explanation
Sometimes the correct alternative really is to do nothing.
That should be expressible without pretending that an empty block is an implementation.
realisedIf (condition) { positiveAction() } else { // do nothing } // SYNTAX ERROR: empty-realised-else // Use wilsonIf to declare intentional inaction. The valid form is:
wilsonIf (condition) { positiveAction() } else nothing The false path is now acknowledged.
It is not justified. It is not proven safe. But it is no longer indistinguishable from a missing piece.
There is an unfortunate naming complication: once we explicitly acknowledge an “unrealized else,” it is no longer unrealized.
The terminology has developed the condition it was intended to diagnose.
Ordinary if remains useful for simple conditional actions:
if (cacheEntryExpired) { removeCacheEntry() } A permissive project allows that. A stricter project can require wilsonIf for nonterminating conditional actions, perhaps only in nested or consequential code.
That preserves the original article’s useful restraint: apply pressure where the missing alternative matters, not everywhere a keyword can be counted.
And the compiler’s knowledge remains limited:
realisedIf (condition) { positiveAction() } else { accomplishAbsolutelyNothing() } There is an executable statement. The structural requirement is satisfied.
Whether it accomplishes anything is a separate investigation. The compiler verifies the paperwork, not the wisdom.
3. Guards do not need an escort
A guard clause states a condition under which execution cannot proceed.
if (!authorized) { throw Unauthorized() } performOperation() The unauthorized path terminates. The authorized path continues.
Adding an else would not discover a missing outcome. It would merely place the main operation inside another block.
if (!authorized) { throw Unauthorized() } else { performOperation() } // SYNTAX ERROR: realised-else // Ordinary if forbids else. // Prefer the terminating guard. The else has been dismissed because its services are no longer required.
This is the strongest case for an anti-else policy: not that alternatives are bad, but that a terminating branch already makes the separation visible.
Our strict acknowledgment policy therefore exempts terminating guards.
There is no requirement to conduct a small ceremony after every return.
4. Three alternatives are not a binary decision
Now consider:
realisedIf (state == READY) { deploy() } else if (state == PENDING) { wait() } else { reject() } // SYNTAX ERROR: strict-binary-decision // Use switch for alternatives over one discriminator. We declared a binary decision and then installed a third outcome.
An else if is an alternative with terms and conditions. We actually submit that we should ban else if completely:
if (condition) { doSomething() } else if (someOtherCondition) { // SYNTAX ERROR: no-else-if // Use separate blocks Because else if is identical to:
if (condition) { doSomething() } if (otherCondition) { doSomethingElse() } For alternatives over one state value, a switch exposes the structure more clearly:
switch (state) { case READY: deploy() case PENDING: wait() default: rejectUnsupportedState(state) } Our language does not fall through by default. Selecting one arm executes that arm.
However, not every chain is secretly a switch:
if (temperature > limit) { shutDown() } else if (batteryLow && disconnected) { conservePower() } Those are arbitrary predicates, potentially overlapping, with an ordering that matters.
Demanding a conventional switch would change the costume without necessarily improving the decision. Our language therefore also offers an ordered predicate case, whose first matching arm wins.
The important distinction is between dispatch over a value and priority among conditions.
A rule that cannot distinguish them has discovered a preference, not a principle.
5. Default is not another word for safe
Requiring a default sounds responsible.
Every possibility gets handled. Nobody falls through a crack.
Unfortunately, “handled” can mean this:
switch (state) { case READY: deploy() default: wait() } Pending, failed, deleted, corrupt, and any state introduced after the author leaves the company will all wait.
The default has excellent coverage and questionable judgment.
Our rule is therefore:
A switch must be exhaustive over its declared domain, or explicitly handle unmatched input.
For a closed set of states, the compiler can require every variant. Adding a new variant then produces an error at decisions that need reconsideration.
For external values, explicit rejection or fallback may be necessary. A type declaration is not a border checkpoint; unexpected runtime values can still arrive through poorly defended boundaries.
The compiler should know whether the author is claiming:
- These are all the possible states.
- Other states are valid and share this behavior.
- Other states are invalid and must be rejected.
Those are different claims.
A default that quietly accepts everything may be the largest secret in the program.
6. Fallthrough must introduce itself
Fallthrough lets a case execute not because it matched, but because it lives downstairs.
There are legitimate uses. Our language permits them, but requires an explicit declaration:
switch (mode) { case FULL: runExtendedChecks() fallthrough case BASIC: runBasicChecks() } Without fallthrough, each arm stops at its boundary.
Grouped labels are different:
case PENDING, QUEUED: wait() Two labels share one body. No body spills into another.
The benefit of explicit fallthrough is not that it prevents every mistake. It makes physical case ordering an acknowledged dependency.
Implicit fallthrough makes that dependency a surprise.
Actual fallthrough into or out of default is prohibited in this language. Default already has a difficult job: explaining what happens when selection finds no match. It should not also serve as a corridor for matched cases.
Shared follow-up work belongs after the switch if it is unconditional, or in an explicitly called helper if it is not.
Default is not the communal kitchen.
7. Logging is not a branching problem
There is one conditional our language rejects for a different reason:
if (debug) { emitLogs() } // SYNTAX ERROR: conditional-logging // Use log.level(...) instead. The answer is not:
wilsonIf (debug) { emitLogs() } else nothing We have clarified the wrong decision.
Logging policy belongs to the logging system:
log.level(DEBUG, "Placement candidate evaluated", candidate) Here, log.level(...) is our fictional logging API: it submits an event at a specified severity. Configuration determines whether that event is emitted.
The application reports what happened. The logger decides which reports are currently wanted.
Otherwise, logging policy becomes distributed across dozens of booleans and branches. During an incident, we discover that the evidence we need was conditional on a flag whose location was also a secret.
Expensive diagnostic construction may still need a lazy logging API or an enabled-level check. That is a resource decision, not a reason to scatter ordinary log filtering throughout the application.
8. What the language knows—and what it does not
Wilson’s Law does not require every alternative to contain work.
It requires the program to distinguish:
- Conditional action.
- Terminating guard.
- Binary choice.
- Intentional inaction.
- Multiway dispatch.
- Ordered predicates.
- Unexpected input.
- Deliberate fallthrough.
A normal language can already express these. Our fictional language makes some of the distinctions compulsory.
A linter could approximate much of this without inventing new keywords: reject redundant else, check exhaustive switches, require explicit fallthrough, flag selected empty branches, and keep ordinary logging policy out of application conditionals.
But neither a compiler nor a linter can establish that every declared decision is a good one.
We can remove every else from a codebase. The conditions will continue to be false.
We can add every missing else. The alternatives may remain unconsidered.
The objective is not more branches or fewer branches.
It is fewer secrets.
The alternative exists whether we write it down or not. The paperwork is optional; the consequences are not.
Appendix
Disclaimers
This post was created with the help of AI. The author takes full responsibility for the content.
Credits
Photo by Zach Reiner on Unsplash