Skip to content

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

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.

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. markRead means “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.

Talk out loud, then a tiny function.

  1. Spec: enqueueNotification({ userId, templateId, at }).
  2. List six failures you might throw (missing user, quiet hours, duplicate, provider down, invalid template, time in the past).
  3. For each, say: erase, mask, aggregate, or surface.
  4. Implement enqueueNotification so at most two explicit error reasons remain.
  5. Name one test per remaining reason, plus one test for a former error you erased.
  • A Philosophy of Software Design — Chapter 10, Define Errors Out Of Existence