Failing by Design
Error handling done at the end is a chore. Error handling done at the start is just design. This chapter is about moving failure to the front of the process, where it's cheap.
The reason error handling feels like drudgery is that we usually add it last — bolting recovery onto code that was shaped entirely around success. But failure states aren't an afterthought to the design; they are part of the design. A "save" feature isn't done when it saves; it's done when you've decided what happens when it can't.
Design the Failure States With the Feature #
- For every feature, list its failure states before writing the happy path.
- Sketch the empty, loading, offline, and error states next to the success state — they are first-class UI, not exceptions.
- Decide the classification up front (see Whose Fault Is It Anyway?) so you know who acts.
Design also means deciding which states are acceptable, which are recoverable, and which must be prevented. A checkout system can honestly support payment_failed, paid_waiting_for_fulfillment, and email_confirmation_delayed. It should not support "charged twice," "paid but no order exists," or "access granted without a payment record."
The same is true when software touches the physical world. If you ship products, lost_in_transit, delivery_delayed, wrong_address, and refund_pending are real states. They are not edge cases just because they happen outside your code. If the business can enter that state, the product needs a way to represent it.
Make Illegal States Unrepresentable #
- Use types and schemas so whole categories of error can't occur.
- Model "loading | error | success" as one value with three shapes, not three loose booleans that can contradict each other.
- Push validation to the boundary so the core never handles bad data — and choose those boundaries deliberately (see Boundaries & Transactions).
You cannot make every real-world failure impossible. You can make the dangerous internal contradictions impossible, or at least visible. "Payment failed" is a normal state. "Payment succeeded but the order disappeared" is a design failure.
Pre-Mortems and Failure Inventories #
- Run a quick pre-mortem: "It's launch day and this feature broke. What happened?"
- Keep a short failure inventory per feature; it becomes your test and measurement list (see Test Once, Measure Always).
- Name the dependencies and ask, for each: what if this is slow, down, or lying?
Budget for Failure Work #
- Estimate the unhappy paths as part of the feature, not as "hardening" later.
- A feature that only works when everything works isn't a smaller feature — it's an unfinished one.
Once you know which failure states are possible and desirable, you can draw the boundaries that keep those states honest. That is the next chapter.
Front End vs. Back End Notes #
- Front end: design loading/empty/error/offline states in the mockup stage; treat them as required screens, not edge cases.
- Back end: define the error contract (status codes, error shapes, idempotency) as part of the API design, before the first endpoint ships.
Related chapters: Failure Modes · Whose Fault Is It Anyway? · Boundaries & Transactions