05 · Define errors out of existence
When you finish this lesson you should be able to:
- Spot special cases that exist only because the interface invited them
- Prefer APIs that make the illegal state hard to represent
- Talk through edges while you build, not as a closing dump
Core idea
Section titled “Core idea”Exceptions and error branches are complexity. Each one is another path a caller must remember. Some failures are real (the network is gone). Many are self-inflicted: the interface allows a question that should never be asked.
Define errors out of existence means changing the operation so the failure does not come up. Delete a missing file? Make delete mean “make sure it is gone,” which is already true if it is missing. Substring past the end? Clamp, or take a range type that cannot be inverted.
When you cannot erase the failure, mask it at a boundary (retry with jitter behind deliver) or aggregate it (one DeliveryResult with many recipient errors, not a throw per row). Crashing is allowed when continuing would corrupt: do not swallow a checksum mismatch and keep charging.
Special cases are cousins of errors. if (user == null), if (flag === 'legacy_sunset_2019'), if (retryCount === 3 && channel === 'sms')—each is a little language the next reader must learn. Design them out: a missing user is Anonymous, a sunset flag is data, retry policy is a table.
Taking it too far: hiding a failure the caller must see (payment declined) behind a generic success. The goal is fewer meaningless branches, not a mute system.
Example
Section titled “Example”Inbox messages have readAt. A shallow API: markRead(id) throws if already read, throws if the id is unknown, throws if the user does not own it.
A calmer API:
- Unknown id or wrong owner: one
notFound(do not leak that the row exists). - Already read: success.
markReadmeans “this message is read,” which is already true. - Bulk mark: return counts, do not throw on the second item.
While coding a parser, mention edges as they appear: empty input, duplicate ids, timestamps in the future, huge lines. Write the tiny test or the if then, not in a two-minute speech at the end. That is the habit: edges are part of the spec, not extra credit.
function markRead(inbox, messageId) { const message = inbox.byId.get(messageId); if (!message) return { ok: false, reason: 'notFound' }; if (message.readAt == null) message.readAt = Date.now(); return { ok: true };}No “already read” error. The state you wanted is the state you have.
Drill (12 minutes)
Section titled “Drill (12 minutes)”Talk out loud, then a tiny function.
- Spec:
enqueueNotification({ userId, templateId, at }). - List six failures you might throw (missing user, quiet hours, duplicate, provider down, invalid template, time in the past).
- For each, say: erase, mask, aggregate, or surface.
- Implement
enqueueNotificationso at most two explicit error reasons remain. - Name one test per remaining reason, plus one test for a former error you erased.
Further reading
Section titled “Further reading”- A Philosophy of Software Design — Chapter 10, Define Errors Out Of Existence