hraness
Theme
Appearance

invalid states: types, result, and parsing from unknown

types that cannot hold a wrong value, and parsers at every edge

by hraness · drafted with ai assistance

the rest of this lesson is free: add your email to keep reading.

In loosely typed systems, the most common bug is a wrong value carried far from where it was created. A string meant to be a path joins a URL. A null passes through three functions before it crashes somewhere unrelated. The Hraness repositories answer this with one convention in three parts: types that cannot hold an invalid state, parsers that turn every foreign value from unknown into a checked type, and failures returned as closed result types instead of thrown.

types that exclude bad states

“Model invalid states out of existence” is a rule about type design. If a session can be pending or established but never both, its type should be a union of those two states. A bag of optional fields allows combinations that mean nothing.

If an operation may only run after verification, the verified value should be a type the unverified one cannot forge. In vhalla, VerifiedEnvelope cannot be cloned or constructed outside decode, so using an unverified envelope as verified produces a program that does not compile. There is no runtime error to catch, and every check removed this way is one that cannot rot.

parsing foreign values from unknown

Types cannot reach the filesystem, the network, a provider response, or a stored record. For those, a foreign value arrives as unknown and gets a type only by passing a parser that validates its shape, rejects unknown keys, and caps every count, byte size, depth, and list.

Rejecting unknown keys matters most. A parser that accepts known fields plus anything else also accepts schema drift: the provider renames id to identifier, and the object still parses, now with id === undefined. With unknown keys rejected, the same rename fails at the boundary the moment the schema changes. Direct’s branded ids apply the rule one level deeper. A RunId is a brand only the parser issues, so a plain string that looks right cannot stand in for one.

The size caps apply the same thinking to volume. A list with no limit is a model of “possibly infinite,” and a system that accepts that will eventually receive it.

returning failures as results

@hraness/result holds the shared pattern for failure. A function that can fail returns a typed result whose variants list the ways it can fail, instead of throwing to whatever code is on the stack. The caller has to handle each variant the compiler knows about, so an unhandled failure is a type error.

Oh’s parsers show all three parts working together. Every stored record and wire value passes a validator that returns either a typed value or a named rejection, and the rest of the kernel never sees an unvalidated shape.

what types still miss

Types model what the author remembered to model. A closed enum covers exactly the cases the author listed, and a state the design forgot is invisible to it. Rejecting unknown keys catches renamed and added fields, but not semantic drift, where the keys stay the same and their meaning changes. That is a modeling problem no type can catch.

Strictness also costs time. Every new legitimate field needs a parser change, every new state needs a union member, and a quick experiment has to satisfy the type system before it runs. The repositories accept that cost rather than pay it later during incidents.

A Result passes a failure up without resolving it. The error type says how the call failed, and the code above it still decides whether to retry, report, or fail closed itself. The type only guarantees that the decision is visible and cannot be skipped.

With all three parts in place, boundaries validate once, the code inside trusts its types, and edges return named failures. What remains to debug is small enough for the rest of this series (property tests, generated sequences, and proofs) to cover.

keep reading: free for subscribers

the rest of this lesson is free. enter your email to subscribe, and every subscriber lesson unlocks in this browser.

already subscribed? enter the same email to unlock.