jonnifyfsharp

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.

C1.08.3 · part 1

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 incoming Error it returns it unchanged, running nothing.
Result.map
chains a plain step 'T -> 'U; on an incoming Error it too passes through untouched.
short-circuit
the first Error bypasses every later step — no step after the failure is run.
the error type
every step shares the same 'TError (here string), 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

parse string → Result n > 0 ? bind n * 10 map input Error track Ok
input "5" · at start · on the Ok track · step to begin
Without JavaScript: each input, where it left the Ok track, and the result
inputparsen > 0 ?n * 10result
"5"Ok 5Ok 5Ok 50Ok 50
"-3"Ok -3ErrorskippedError "must be positive"
"x"ErrorskippedskippedError "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.

C1.08.3 · part 2

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

Related in this course