Part 10 – Errors as Values
This is a series of posts I’m writing about using types as another tool in software development, Continuous Delivery, & keeping LLM’s honest. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.
Parts
- Part 1 – Branded Types
- Part 2 – Product Types
- Part 3 – Union & Discriminated Unions
- Part 4 – Non-Empty Collections
- Part 5 – Indexed Types
- Part 6 – unknown vs any
- Part 7 – Result
- Part 8 – Schema
- Part 9 – Total Function
- Part 10 – Errors as Values
- Part 11 – Property Tests
- Part 12 – Type Proofs
- Part 13 – Exhaustiveness Checking
- Part 14 – Parse, Don’t Validate
- Part 15 – Anti-Corruption Layer
- Part 16 – Opaque Types
- Part 17 – Maybe
- Part 18 – Smart Constructors
- Part 19 – Pipeline
- Part 20 – Railway Oriented Programming
- Part 21 – Typestate
- Part 22 – Capabilities
- Part 23 – Immutability
- Part 24 – Making Impossible States Impossible
- Part 25 – Type Driven Development: How to do it
- Part 26 – Final Thoughts


Errors as Values moves errors from a random, runtime consequence to an iterative design process using types. u intentionally think about & design what could go wrong w/your code in functions, libraries/back-ends u integrate with & your application. Let’s fix fetch to see how this all works.
We’ve talked about the 4 error types in the past:
- domain
- infrastructure
- panic
- unforeseen
If u don’t know where an Error goes in the above list, 1st can u or the user do anything about it? If no, then 3.
2nd, ask your Product/Business Owner. If they look confused it’s 2, else it is 1.
fetch is infrastructure; we use it to make http calls, but it’s painful to work w/; Promise<Response> as a return value lies, having to check response.ok first, _then_ attempt response.json(), etc. Numerous exceptions, order of operation issues, none of it type-safe. 1 thing can go right: Your schema passes. Let’s narrow what can go wrong: only 4 things: it timed out, got a 4xx/5xx status, a network http 0 error, or your schema failed. Let’s model that as a Discriminated Union to narrow what can go wrong:
type FetchError = Timeout | BadStatus | NetworkErr | BadBody
Why not just a Union? When we get a BadStatus, what was the status code? When we get a BadBody was it the JSON failing to parse, or it parsed by didn’t match the schema, & on what part(s)? DU’s hold data + ensure we only access that data for that particular type.
type Timeout = { tag: 'Timeout', time: Seconds }
type BadStatus = { tag: 'BadStatus', code: HttpStatusCode }
type NetworkErr = { ... }
type BadBody = { tag: 'BadBody', zodErrors: ZodError[] }Code language: JavaScript (javascript)
Now we can write a more total, safer function to fetch data:
type get = <T>(s:ZodSchema<T>, url:string) =>
Result<T, FetchError>Code language: HTML, XML (xml)
Then you wrap fetch with code to convert to our 4 errors:
try {
const response = fetch(...)
// handle bad status
if(!response.ok) {
return { tag: 'BadStatus', code: response.status }
// handle timeouts
if(error.name === 'AbortError') {
return { tag: 'Timeout', ... }Code language: JavaScript (javascript)
Benefits of this hard work? Fetch is no longer imperative & its now type-safe. A single switch statement handles only 4 unhappy paths:
switch(r.tag) {
case 'Timeout': ...
case 'BadStatus': ...
case 'NetworkErr': ...
case 'BadBody': ...Code language: PHP (php)
BUT you still have the option to retry, convert to another error type, or recover, all in a type-safe way.
Our Product Owner wants a new Domain Error if account is not activated:
type NotActivated = { tag: 'NotActivated' }
if(!account.activated) {
return err({ tag: 'NotActivated' })Code language: JavaScript (javascript)
Even in happy path, we can have errors; we model them intentionally in our Domain, the compiler ensures we handle them as our code grows.
Leave a Reply