SkillAgentSearch skills...

Railroad

A terse Haskell railroad error handling DSL abstracting over error functors by catamorphism via Either

Install / Use

/learn @mastratisi/Railroad

README

railroad

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.

Haskell License


Why railroad?

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 c

railroad 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 guard

Install

Nix

Here 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   
  }) {};

Hackage

The package is under the name railroad. Link: https://hackage.haskell.org/package/railroad

Operators

| 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:

  • Bool
  • Maybe a
  • Either e a
  • Validation e a
  • Traversable containers of the above ([Maybe a], Vector (Either e a), …)

Design Highlights

  • Bifurcate typeclass turns any “railroad-shaped” functor into Either (CErr f) (CRes f) via catamorphism
  • Native support for Effectful (Error effect) and MTL (MonadError) via Railroad.MonadError
  • Short-circuiting for Either, error accumulation for Validation

See railroad.md for a deeper tour, or just read the code at Railroad.hs. It's a short file.

Comparison with hoist-error

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.

Quick Feature Comparison

| 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 |

When to choose which?

Choose hoist-error if you want:

  • The absolute smallest API and dependency footprint
  • Only need simple Maybe/Either/ExceptT hoisting
  • Classic mtl or transformers codebases

Choose Railroad if you want:

  • A full DSL that feels built into the language
  • Excellent Validation support (error accumulation)
  • Frequent collection checks or predicate guards
  • Maximum readability in Effectful applications

Contact

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.

Related Skills

View on GitHub
GitHub Stars6
CategoryDevelopment
Updated13d ago
Forks0

Languages

Haskell

Security Score

90/100

Audited on Mar 26, 2026

No findings