Dive C1.08.3
Railway-oriented errors
Two tracks: the Ok track runs the work, and the first Error switches the whole train onto the error track.
Railway-oriented programming is a widely-used F# name for a simple picture.
A multi-step computation is a railway with two parallel tracks: a success track and a failure track. Each step
is a function that returns a Result, and Result.bind joins the
steps so that as long as a step returns Ok the train stays on the success track, but
the first step that returns Error throws a switch — the train moves to the
failure track, and every later step is skipped. The error rides through to the end unchanged.
This dive builds a three-step pipeline and watches the switch throw.
Three steps, joined by bind
The pipeline below validates an input in three steps. First parse (from the previous
dive) turns the string into a Result<int,string>. Then
Result.bind runs a positive-check that itself returns a
Result: it passes a positive number along as Ok, and rejects
anything else as Error "must be positive". Finally
Result.map multiplies the surviving value by ten.
let validate input = parse input |> Result.bind (fun n -> if n > 0 then Ok n else Error "must be positive") |> Result.map (fun n -> n * 10) validate "5" // val it : Result<int,string> = Ok 50 validate "-3" // val it : Result<int,string> = Error "must be positive" validate "x" // val it : Result<int,string> = Error "not an int: x"
Read the three outcomes against the steps. "5" parses, is positive, and is
multiplied: Ok 50. "-3" parses but fails the positive-check,
so the switch throws at step two and the multiply never runs: Error "must be positive".
"x" fails to parse at step one, so both later steps are skipped and the original
parse error rides through: Error "not an int: x".
- Result.bind
- chains a step of type
'T -> Result<'U,'E>; on an incomingErrorit returns it unchanged, running nothing. - Result.map
- chains a plain step
'T -> 'U; on an incomingErrorit too passes through untouched. - short-circuit
- the first
Errorbypasses every later step — no step after the failure is run. - the error type
- every step shares the same
'TError(herestring), so any step's failure fits the one error track.
Run validate input
Pick an input, then step the train through the pipeline. It starts on the Ok track; at the first step that fails, the switch throws to the Error track and the remaining steps go dim — they are skipped. The readout names the step reached and the exact FSI result.
parse → positive? → ×10 · the switch throws at the first Error
| input | parse | n > 0 ? | n * 10 | result |
|---|---|---|---|---|
| "5" | Ok 5 | Ok 5 | Ok 50 | Ok 50 |
| "-3" | Ok -3 | Error | skipped | Error "must be positive" |
| "x" | Error | skipped | skipped | Error "not an int: x" |
The failing step fixes the result, and nothing after it runs. One match
at the end reads Ok or Error — the error message already
names which step threw the switch.
Why the shape holds up
The railway is how real F# programs handle validation and parsing: a request flows through a chain of checks,
and the first failure stops the chain with a precise reason. Because each step has the same
Result<_,'TError> shape, the steps compose with bind and
map and the whole pipeline is one expression — no early return,
no nested if, no thrown exception to catch three frames up.
The two-track picture is the well-known teaching image for this style; the mechanism under it is exactly the
Result module from the previous dive. The C1.09 lab generalises the pattern: a
computation expression can hide the bind calls behind
let!, so a railway reads like ordinary sequential code while keeping the
short-circuit.
The idea
Compose failable steps; the first failure stops the rest and carries its reason to the end.
F# today
A chain of Result.bind / Result.map — the railway, in one expression.
Where this leads
This closes the language spine. Option and Result turn
absence and failure into values; the railway composes them. The final module, C1.09, gives the
bind-threading its own syntax with computation expressions — a small language
built from the same parts.
References
Sources
- Microsoft — F# documentation — error handling without exceptions, and chaining results.
- FSharp.Core API reference — the
Resultmodule:bindandmap, the connectors of the railway. - The F# Software Foundation — the language's home and its learning resources.
Related in this course
- C1.08.2 — Result — the type and the
bindthe railway runs on. - C1.08 — Option, Result & railway-oriented errors — the module hub.
- Elixir — Purity — the BEAM's
withchain on{:ok, _}/{:error, _}, the same short-circuit.