Part 20 – Railway Oriented Programming
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


The issue with using Pipeline programming is it only works for happy paths or data transformations. For everything else, there are failures/Result<T, E>’s involved, and it’s quite painful to open them up and do logic on each function in turn.
safeFetch(...)
.then(r => {
// if everything is ok, take out the value,
// and continue the pipeline with parsing
if(r.isOk()) {
return safeParse(r.value, schema)
// otherwise, keep returning the error result
// so it'll pop out at the end
} else {
return r
}
})Code language: JavaScript (javascript)
Even if 9 of our functions are pure and have no errors, if we connect it to a 10th that could have a failure, now the entire pipeline could fail.
This is where Railway Oriented Programming comes in. You design a Pipeline where there are 2 tracks; the green happy path, and the red unhappy path, made specifically for the Result type. If any function returns an Err<E> type, it does not process the next function in the pipeline, and instead, just returns the err(E) value, basically short circuiting the rest. Otherwise, if everything is good as an Ok<T>, it keeps passing the value through the Pipeline as normal.
safeFetch(...)
.andThen( safeParse )
.andThen( cowSchema )Code language: CSS (css)
This solves a lot of problems:
- unlike Promise, it doesn’t throw exceptions; your Pipeline can continue to be a total function
- you can define the error type the pipeline can return, Promise is just
Promise<T>vs.Result<T, E> - all
Result<T, E>style functions can suddenly be composed together easily - functions that can’t fail, you can just return an
ok()with their return value
You get all the benefits of Pipeline, with type-safe error handling. This powerful abstraction is how you build larger functions, and in turn an entire program that is type-safe.
That said, you have 2 escape hatches needed for the real world. The first is _unsafeUnwrap/_unsafeUnwrapErr for unit tests so you don’t have to type narrow to get the value/error out just to test something. Also comes with a linter to ensure you aren’t using _unsafe* functions in production code.
cow1 = ok(1)._unsafeUnwrap()
// will explode if it were an err('b00m')Code language: JavaScript (javascript)
The 2nd is .match; say for example you’re at the top of an AWS Lambda and need to return an HTTP response as JSON, or throw if you’re panicking:
result.match(
data => ({
statusCode: 200,
body: JSON.stringify(data)
}),
(error:CustomError | Panic) => {
if(error.tag == 'CustomError'){
return ({
statusCode: 500,
body: error.message
})
}
throw error
)Code language: JavaScript (javascript)
For TypeScript, you have Neverthrow or Effect and there are options for other languages, if it’s not built-in, to make Railway Programming easier.
Leave a Reply