Part 7 – Result
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.
Good type systems create pure & total functions. Where does that leave errors? In programming, we have 4 types of errors:
- those we create in our domain
- those we expect from infrastructure
- situations where we cannot recover
- those we weren’t expecting, hence “exceptional” situations


If a type system has Discriminated Unions (sum / variant types), then that’s what we use for 1 & 2, the majority of what programmers deal w/.
type FetchError
= BadURL
| BadBody
| BadStatus
| NetworkError
| Timeout
Unless you’re dealing w/important hardware situations like Rust’s panics, we just ignore 3 b/c you can’t do anything anyway. 4 will “go away” if you focus on 1 & 2.
Modelling errors w/Unions poses a problem from a Domain Driven Design perspective: how do you differentiate 2 paths; happy & unhappy? The FP world has settled on a Result type:
type Result<T, E>
= Ok<T>
| Err<E>Code language: HTML, XML (xml)
It has 2 type parameters; T type for happy path data, E error type for unhappy path.
type FetchResult<Array<Person>, FetchError>Code language: JavaScript (javascript)
However, E in practice never is just 1 error for long. As you start composing more functions together that can fail, you start creating anonymous unions. For example, if we read from an environment variable for our secrets:
type ReadSecrets = () =>
Result<Secrets, ReadError>Code language: JavaScript (javascript)
… then call fetch, our return type for that function would be both errors:
type fetchPeople = () =>
Result<Array<Person>, ReadError | FetchError>Code language: JavaScript (javascript)
Those anonymous unions of errors, e.g. ReadError | FetchError, start happening a lot. The type system ensures through exhaustiveness checking you handle all errors. You also discover more about your domain through what can go wrong.
Testing is straight forward b/c the happy path is just Ok<T>:
stubFetch = () => ok([ person ])Code language: JavaScript (javascript)
& unhappy is an Err<E>:
stubBadFetch = () => err(b00m b00m)Code language: JavaScript (javascript)
You still have the flexibility to recover from each error; e.g. if secrets can’t be read from environment variables, you can fetch from a service. Others, like a fetch error BadStatus<429>, you could retry. Why not try/catch? Result is type safe + ensures all are handled.
Result gives you a convention for error handling + separating happy/unhappy paths; u still retain the ability to create DDD Domain errors “PersonUnauthorized”, infrastructure “FetchError”, & Panics like “CantReadSecrets” that u can map to a throw/panic (or Effect.die). You no longer use try/catch unless you’re mapping known unsafe (or potentially unsafe) code to a Result:
try {
const json = JSON.parse(data)
return ok(json)
} catch(error) {
return err(error)
}Code language: JavaScript (javascript)
We’ll talk more in the future about how to model Errors as Values & build applications on top of Result, and the benefits of doing so.
Leave a Reply