Skip to content

ROP Reference

Railway Oriented Programming (ROP) is a functional error handling pattern proposed by Scott Wlaschin. Every function returns either a success (Success) or failure (Failure) track, and when a failure occurs, subsequent steps are skipped and the failure propagates along the failure track. Functorium’s FinResponse<A> is a C# implementation of this ROP pattern.


Success track: ──── f1 ────── f2 ────── f3 ────── Result
│ │ │
Failure track: ──────────────────────────────────── Error

Every function has two outputs:

  • Success (Succ): Passes the value to the next function
  • Failure (Fail): Skips remaining functions and propagates the error

Each function acts like a “switch (branching point)“:

Input → [Function] → Success output (to next step)
└→ Failure output (to failure track)

// Success track
FinResponse<A>.Succ(value) // contains value
// Failure track
FinResponse<A>.Fail(error) // contains error

FinResponse<A> is always in one of these two states. It represents the two tracks as a Discriminated Union in C#.

result.Match(
Succ: value => $"Success: {value}", // Success track handling
Fail: error => $"Failure: {error}"); // Failure track handling

Match executes a different function depending on which track the value is currently on.

FinResponse<int> length = name.Map(s => s.Length);
Success track: ──── "hello" ──── [Map: s.Length] ──── 5
Failure track: ──── error ──────────────────────────── error (passed through)

Map transforms only the value on the success track. If on the failure track, the function is not executed and the error is passed through as-is.

FinResponse<User> user = userId
.Bind(id => FindUser(id)) // FindUser may fail
.Bind(u => ValidateUser(u)); // ValidateUser may fail
Success track: ──── id ──── [FindUser] ──── user ──── [ValidateUser] ──── validUser
│ │
Failure track: ──────────────── error ──────────────────── error

Bind connects switch functions. If any step fails, subsequent steps are skipped.

Using C#‘s LINQ query syntax, Bind chains can be written more readably:

// LINQ syntax (easy to read)
var result =
from request in Validate(input)
from product in CreateProduct(request)
from saved in SaveProduct(product)
select saved;
// The above is equivalent to
var result = Validate(input)
.Bind(request => CreateProduct(request))
.Bind(product => SaveProduct(product));

This is possible because FinResponse<A> implements Select and SelectMany:

public FinResponse<B> Select<B>(Func<A, B> f) => Map(f);
public FinResponse<C> SelectMany<B, C>(
Func<A, FinResponse<B>> bind,
Func<A, B, C> project) =>
Bind(a => bind(a).Map(b => project(a, b)));

Mediator Pipelines have a structure similar to ROP:

Request → [Validation] → [Logging] → [Tracing] → [Handler] → Response
│ │ │ │
└──── Fail ──┴─── Fail ──┴─── Fail ──┘

Each Pipeline acts like a switch:

  • Success: Calls the next Pipeline (next())
  • Failure: Returns a failure response via TResponse.CreateFail(error) and skips subsequent Pipelines

Both types support ROP, but they differ critically in whether they can be used as Pipeline constraints.

ItemFin<T> (LanguageExt)FinResponse<A> (Functorium)
Typesealed structabstract record
Pipeline constraintNot possible (sealed struct)Possible (implements interfaces)
ROP methodsMatch, Map, BindMatch, Map, Bind
LINQ supportOO
Conversion-Fin<A>.ToFinResponse()
FactoryFin.Succ, Fin.FailFinResponse.Succ, FinResponse.Fail

Fin<T> is used in the Repository layer, while FinResponse<A> is used in the Usecase/Pipeline layer. The ToFinResponse() extension method bridges these two layers.



The following appendix compiles key terms related to generic variance, type systems, functional programming, and architecture used in this tutorial.

Appendix D: Glossary