A terse Haskell railroad error handling DSL abstracting over error functors by catamorphism via Either.
railroad is a tiny, opinionated library that brings Railway Oriented Programming (ROP) to Haskell.
It gives you a clean, linear DSL for collapsing error-carrying values
(Maybe, Either, Validation, Bool, and collections thereof)
directly into your MonadError (or Effectful.Error) context — no
noisy case expressions, no repeated either/maybe boilerplate.
Typical error handling in Haskell quickly turns into a tarpit of accidental controlflow:
do
user <- case fetchUser uid of
Nothing -> throwError UserNotFound
Just u -> pure u
cfg <- case loadConfig path of
Left e -> throwError (toConfigError e)
Right c -> pure crailroad replaces all of that with a single, consistent operator
family that reads linearly:
do
user <- fetchUser uid ? UserNotFound -- Maybe / Either / Validation / Bool
cfg <- loadConfig path ?? toConfigError -- custom error mapping
items <- queryItems filters ?+ NoResultsFound -- cardinality guardHere is an expression you can add to ghcWithPackages:
railroad = pkgs.haskellPackages.callCabal2nix "railroad" (pkgs.fetchFromGitHub
{ owner = "mastratisi"
; repo = "railroad"
; rev = "master"
; sha256 = pkgs.lib.fakeSha256; # Nix will tell you the real hash on first build
}) {};The package is under the name railroad. Link: https://hackage.haskell.org/package/railroad
| Operator | Purpose | Example |
|---|---|---|
?? |
Main collapse with custom error mapping | action ?? toMyError |
? |
Collapse to constant error | action ? MyError |
?> |
Predicate guard | val ?> isBad toErr |
??~ |
Recover with a mapped default (error → value) | action ??~ toDefaultVal |
?~ |
Recover with a fixed default value | action ?~ defaultVal |
?+ |
Require non-empty collection | items ?+ NoResults |
?! |
Require exactly one element | items ?! fromCardinalityErr |
?∅ |
Require empty collection (alias ?@) |
items ?∅ DuplicateFound |
All operators work uniformly on:
BoolMaybe aEither e aValidation e a- Traversable containers of the above (
[Maybe a],Vector (Either e a), …)
- Bifurcate typeclass turns any “railroad-shaped” functor into
Either (CErr f) (CRes f)via catamorphism - Native support for Effectful (
Erroreffect) and MTL (MonadError) viaRailroad.MonadError - Short-circuiting for
Either, error accumulation forValidation
See railroad.md for a deeper tour, or just read the
code at Railroad.hs. It's a short file.
Both hoist-error
and Railroad are small, focused Haskell libraries that make error
handling in MonadError (or Error effects) far more pleasant than raw
case expressions, either, or maybe. They let you collapse partiality
structures (Maybe, Either, etc.) into a linear monadic flow unpacking
values while still controlling how errors are turned into your
application’s error type.
They solve the same core pain point—“railroad-style” error handling where the happy path stays on the main track and errors branch off cleanly—but they do it with different levels of abstraction and power.
| Feature | hoist-error | Railroad |
|---|---|---|
| Core mechanism | PluckError class + hoistError |
Bifurcate type family + catamorphism to Either |
| Supported functors | Maybe, Either e, ExceptT e m |
Bool, Maybe, Either, Validation, traversables of any of the above |
| Collection handling | None (manual) | Built-in cardinality: ?+ (non-empty), ?! (exactly one), ?∅ (must be empty) |
| Error mapping | Function or constant (<%?>, <?>) |
Function (??) or constant (?) with typed CErr constructors |
| Recovery / defaults | No built-in | ?~ / ??~ (constant or computed fallback) |
| Guards | No | ?> (predicate → error) |
| Monadic hoisting | <%!?>, hoistErrorM |
Handled naturally by >>= + ?? |
| Primary ecosystem | Any MonadError / ExceptT / mtl |
Effectful Error (with full MonadError re-export) |
| Dependencies | Minimal (base + mtl/transformers) |
effectful, validation, mtl |
| Learning curve | Very low (just a few operators) | Slightly higher (8 operators, but ?? and ? cover ~90%) |
| Size | ~150 LOC | ~150 LOC |
Choose hoist-error if you want:
- The absolute smallest API and dependency footprint
- Only need simple
Maybe/Either/ExceptThoisting - Classic
mtlortransformerscodebases
Choose Railroad if you want:
- A full DSL that feels built into the language
- Excellent
Validationsupport (error accumulation) - Frequent collection checks or predicate guards
- Maximum readability in
Effectfulapplications
Feel free to DM me on https://x.com/mastratisi97 . I find it interesting to know if other people also have found this module useful.