Skip to content

Revision — The Edges, One More Time

The overview promised a field guide, not a bag of war stories — a machine for producing tests. We have now walked every family of input and state that reality hands your code, from the humble integer to the trust boundary at the edge of your process. Before you close the guide, it is worth walking back through it once at altitude, so the eight pages collapse into the single move that runs all of them.

That move is the same one you started with. For any input or state, stop being the author who hopes it works and become the attacker who wants it to fail. Then do three things, mechanically: pick a category, enumerate its boundaries, turn each boundary into a test. Everything below is that sentence, applied eight times.

The one lens: “how would I attack this?”

Section titled “The one lens: “how would I attack this?””

Every page in this Part answered one question aimed at a different target:

For this category of input or state, where are the boundaries — and what does the code do when I hand it one?

The reason this feels adversarial is that you are deliberately generating the inputs the author would never send. The author pictures a short list of ordinary things, one at a time, on a calm machine. You send the empty list, the value one past the maximum, the second identical request, the timezone that has no 2:30 a.m. tonight. You are not being creative — you are being systematic, because underneath every category is the same tiny grid:

value : ... negative ── 0 ── 1 ... max ── max+1(overflow)
size : 0(empty) ── 1(singleton) ── many ── huge
sequence: first ── middle ── last ── repeat(again)

Almost every edge in this guide is one cell of that grid, applied to one family of inputs. Enumeration stops being inspired and becomes automatic once you accept that the bug is nearly always at a neighbor — never in the comfortable middle where the happy path lives.

The eight families, recapped as boundary problems

Section titled “The eight families, recapped as boundary problems”

The guide split the world into categories not because they are unrelated, but because each one carries its own short, known list of edges. Group them by what they attack.

The first three content pages attacked the arguments — the raw values that flow into a function.

  • Numbers are the purest boundary problem. The edges are zero (the divisor, the empty average, the identity that changes behavior), negative (the quantity, the index, the refund the author never pictured), overflow (max + 1, where the type silently wraps), and floats (where 0.1 + 0.2 != 0.3 and equality is a lie you must replace with a tolerance). Every one is a cell on the value row of the grid.
  • Strings are a collection of characters, so they inherit the size row — empty (the string that breaks your “first character” logic) and huge (the megabyte pasted into a name field) — and then add two edges of their own: Unicode (a “character” is not a byte; combining marks, emoji, and normalization forms mean len lies and case-folding is locale-dependent) and injection (untrusted text that stops being data and becomes code — '; DROP TABLE, <script>, a path with ../).
  • Collections are the size and sequence rows made explicit. The canonical enumeration is 0, 1, many — empty (no first element, no separator to place), singleton (no second element, so your join or “and” logic never runs), and many (the general case). On top of size sit duplicates (does a repeat break your set-like assumption?) and order (did the code silently assume the input was sorted, or that iteration order was stable?).

The two domains where intuition fails hardest

Section titled “The two domains where intuition fails hardest”

The middle two pages attacked time and nothingness — the categories where a confident author is most often wrong.

  • Dates and times are a boundary minefield because the number line of instants is warped by human convention. The edges: UTC vs. local (store and compute in UTC, format at the edge), DST (the spring-forward hour that does not exist and the fall-back hour that happens twice), leap years and leap seconds (Feb 29 exists only sometimes; a minute can have 61 seconds), and epoch boundaries (the 32-bit Unix timestamp that overflows in 2038, the direct descendant of Y2K). Time is where “it’s just a number” is most seductive and most false.
  • Null, optional, and absence attack a distinction the type system may not force you to see: null vs. missing vs. empty vs. default. A field that is null, a field that is absent from the payload, an empty string, and the value 0 are four different states, and code that conflates them ships bugs — the classic being “no value” silently treated as “zero,” so a missing discount becomes a free order. The edge is not a value at all; it is the absence of one, and it needs its own enumerated tests.

The last two content pages stopped attacking the arguments and started attacking the environment — simultaneity, unreliable hardware, and untrusted boundaries.

  • Concurrency breaks the assumption that runs quietly under every prior page: one thing at a time. Its edges are interleavings (two operations whose steps can be shuffled, so a read-modify-write loses an update), deadlock (two actors each holding what the other needs), and TOCTOU — time-of-check-to-time-of-use, where the world changes in the gap between checking a condition and acting on it. These bugs live in the sequence row, but the sequences belong to different threads, which is why they hide from single-threaded tests.
  • Resources, network, and trust attack the comfortable fiction that the machine always says yes. The edges: resources (the full disk, the exhausted file handle, the out-of-memory allocation), network (the dropped packet, the timeout, the response that arrives twice, the partial write), state (the process that crashes mid-transaction and restarts into a half-finished world), and trust (every place data crosses from untrusted to trusted — the input you must validate because you did not create it). This is where the edges stop being about the value and start being about the world’s refusal to be reliable.
VALUES numbers ── zero · negative · overflow · float
strings ── empty · huge · unicode · injection
collections ─ 0 · 1 · many · duplicates · order
HARD DOMAINS dates ── UTC · DST · leap · epoch(2038)
absence ── null vs missing vs empty vs default
THE WORLD concurrency ─ interleavings · deadlock · TOCTOU
resources ── disk · network · state · trust

Eight families, one lens. The grouping is a memory aid, not a hierarchy — when you meet a real input, you rarely know in advance which family will bite, so you run the whole checklist.

Here is the part that turns a guide into an asset. Enumerating boundaries is only half the discipline; the other half is the standard the overview set and every page kept:

When you find an edge, you do not just fix it — you name it and freeze it as a regression test.

A one-off fix repairs today’s bug and forgets it. A named regression test — rejects_quantity_one_past_the_maximum, parses_two_thirty_am_on_spring_forward_night, treats_missing_discount_as_no_discount_not_zero — does three things at once. It fails loudly if that exact bug ever returns. It documents, in its name, what edge exists in a way that a comment never survives to do. And it accumulates: every attack you run leaves behind a permanent assertion, so the boundaries you probed once are guarded forever.

This is why the guide’s output is not a feeling of paranoia but a growing suite. Each page handed you a short list of edges; each edge you turned into a test is one more line of the map of where this code lives dangerously. The confidence you end with is justified precisely because you can point at the tests that earned it — one per boundary, each named after the failure it prevents.

You began this Part able to test the happy path — the one region that was never in doubt. You end it with a repeatable procedure for the regions that were. Hand yourself an unfamiliar function and you no longer stare at it hoping to be clever; you ask which families of input does this touch, pull each family’s short list of edges from this guide, and write one named test per boundary. The cleverness is now in the catalogue, not in the moment.

That is the whole field guide in one line: for any input or state, ask how you would attack it, enumerate the boundaries, and make each boundary a named regression test — so the edges you find stop being surprises and start being the durable, justified confidence that your software works even where reality is unkind.