{"id":47595520,"url":"https://github.com/ivelten/monad-rail","last_synced_at":"2026-04-23T18:04:37.584Z","repository":{"id":343862181,"uuid":"1179356169","full_name":"ivelten/monad-rail","owner":"ivelten","description":"Railway Oriented Application Library for Haskell.","archived":false,"fork":false,"pushed_at":"2026-03-12T03:29:05.000Z","size":10,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-12T10:10:30.146Z","etag":null,"topics":["error-handling","haskell","railway-oriented-programming"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ivelten.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-12T00:21:26.000Z","updated_at":"2026-03-12T03:29:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ivelten/monad-rail","commit_stats":null,"previous_names":["ivelten/monad-rail"],"tags_count":1,"template":false,"template_full_name":"ivelten/haskell-devcontainer-template","purl":"pkg:github/ivelten/monad-rail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivelten%2Fmonad-rail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivelten%2Fmonad-rail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivelten%2Fmonad-rail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivelten%2Fmonad-rail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ivelten","download_url":"https://codeload.github.com/ivelten/monad-rail/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivelten%2Fmonad-rail/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290741,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["error-handling","haskell","railway-oriented-programming"],"created_at":"2026-04-01T18:01:30.621Z","updated_at":"2026-04-01T18:01:34.565Z","avatar_url":"https://github.com/ivelten.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# monad-rail\n\n\u003e **Note:** This is a vibe coded project for personal experience.\n\n[![Hackage](https://img.shields.io/hackage/v/monad-rail.svg)](https://hackage.haskell.org/package/monad-rail)\n[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](LICENSE)\n\nRailway-Oriented error handling for Haskell.\n\n`monad-rail` implements [Railway-Oriented Programming (ROP)](https://fsharpforfunandprofit.com/rop/) — a functional pattern that makes error handling explicit and composable. Your computation runs on two tracks: success and failure. Once on the failure track, execution stops — unless you use `\u003c!\u003e` to run multiple validations in parallel and collect all their errors at once.\n\n## Installation\n\nAdd to your `.cabal` file:\n\n```cabal\nbuild-depends:\n  monad-rail ^\u003e=0.1.0.0\n```\n\n## Quick Start\n\n### 1. Define your error type\n\nImplement `HasErrorInfo` with `errorPublicMessage` — the only required method. Derive `Data` to get an automatic error code from the constructor name:\n\n```haskell\n{-# LANGUAGE DeriveDataTypeable #-}\n\nimport Monad.Rail\n\ndata UserError\n  = NameEmpty\n  | EmailInvalid\n  | AgeTooLow\n  deriving (Show, Data)\n\ninstance HasErrorInfo UserError where\n  errorPublicMessage NameEmpty    = \"Name cannot be empty\"\n  errorPublicMessage EmailInvalid = \"Invalid email format\"\n  errorPublicMessage AgeTooLow    = \"Must be at least 18 years old\"\n-- NameEmpty    → { message: \"Name cannot be empty\",           code: \"NameEmpty\" }\n-- EmailInvalid → { message: \"Invalid email format\",           code: \"EmailInvalid\" }\n-- AgeTooLow    → { message: \"Must be at least 18 years old\",  code: \"AgeTooLow\" }\n```\n\nWhen you need custom codes or extra per-constructor behaviour, override individual methods — see [`HasErrorInfo`](#haserrorinfo) for the full pattern.\n\n### 2. Write your validations\n\n```haskell\nvalidateName :: String -\u003e Rail ()\nvalidateName name\n  | null name = throwError NameEmpty\n  | otherwise = pure ()\n\nvalidateEmail :: String -\u003e Rail ()\nvalidateEmail email\n  | '@' `notElem` email = throwError EmailInvalid\n  | otherwise = pure ()\n\nvalidateAge :: Int -\u003e Rail ()\nvalidateAge age\n  | age \u003c 18  = throwError AgeTooLow\n  | otherwise = pure ()\n```\n\n### 3. Accumulate errors with `\u003c!\u003e`\n\n```haskell\nvalidateUser :: String -\u003e String -\u003e Int -\u003e Rail ()\nvalidateUser name email age = do\n  validateName name \u003c!\u003e validateEmail email \u003c!\u003e validateAge age\n  -- All three run regardless of failure.\n  -- If any fail, ALL errors are collected before stopping.\n  saveUser name email age\n```\n\n### 4. Run and handle results\n\n```haskell\nmain :: IO ()\nmain = do\n  result \u003c- runRail (validateUser \"\" \"not-an-email\" 16)\n  case result of\n    Right () -\u003e\n      putStrLn \"User saved!\"\n    Left errors -\u003e\n      -- Prints all 3 errors as a JSON array\n      print errors\n```\n\nOutput:\n\n```json\n[\n  {\"message\":\"Name cannot be empty\",\"code\":\"NameEmpty\"},\n  {\"message\":\"Invalid email format\",\"code\":\"EmailInvalid\"},\n  {\"message\":\"Must be at least 18 years old\",\"code\":\"AgeTooLow\"}\n]\n```\n\n## Core Concepts\n\n### `Rail a`\n\nThe main type alias for railway computations:\n\n```haskell\ntype Rail a = RailT Failure IO a\n```\n\nUse `RailT` directly if you need a different base monad.\n\n### `throwError`\n\nMoves execution to the failure track with a single error:\n\n```haskell\nthrowError :: (HasErrorInfo e, Show e, Typeable e) =\u003e e -\u003e RailT Failure m a\n```\n\nAll subsequent steps in the `do`-block are skipped.\n\n### `\u003c!\u003e` (error accumulation)\n\nThe key operator for Railway-Oriented Programming. Runs **both** sides regardless of failure and combines the errors:\n\n| Left | Right | Result |\n| --- | --- | --- |\n| `Right` | `Right` | `Right` — continue |\n| `Left e1` | `Right` | `Left e1` — stop |\n| `Right` | `Left e2` | `Left e2` — stop |\n| `Left e1` | `Left e2` | `Left (e1 \u003c\u003e e2)` — stop, both errors |\n\nIdeal for form validation, configuration checks, and any scenario where you want to report all problems at once.\n\n### `tryRail`\n\nWraps any IO action that may throw exceptions and lifts it into the Railway:\n\n```haskell\ntryRail :: HasCallStack =\u003e IO a -\u003e Rail a\n```\n\nIf the action throws, the exception is caught and converted to a `SomeError` wrapping an `UnhandledException`. This lets you bring ordinary IO operations into a Railway pipeline without manual exception handling.\n\n```haskell\n-- File operations\nreadConfig :: FilePath -\u003e Rail String\nreadConfig path = tryRail (readFile path)\n\n-- Combined with validations\npipeline :: FilePath -\u003e Rail ()\npipeline filePath = do\n  content \u003c- tryRail (readFile filePath)\n  validateName content \u003c!\u003e validateEmail content\n  saveToDb content\n```\n\n### `tryRailWithCode`\n\nLike `tryRail`, but lets you derive a domain-specific error code from the caught exception:\n\n```haskell\ntryRailWithCode :: HasCallStack =\u003e (SomeException -\u003e Text) -\u003e IO a -\u003e Rail a\n```\n\nPass a constant function when the code is fixed, or inspect the exception to return different codes:\n\n```haskell\ntryDb :: HasCallStack =\u003e IO a -\u003e Rail a\ntryDb = tryRailWithCode (const \"DbError\")\n\ntryHttp :: HasCallStack =\u003e IO a -\u003e Rail a\ntryHttp = tryRailWithCode $ \\ex -\u003e\n  if \"timeout\" `T.isInfixOf` T.pack (displayException ex)\n    then \"HttpTimeout\"\n    else \"HttpError\"\n\npipeline :: Rail ()\npipeline = do\n  user \u003c- tryDb   (queryUser userId)\n  resp \u003c- tryHttp (fetchProfile user)\n  pure ()\n```\n\n\u003e **Note:** add `HasCallStack` to your wrapper's own signature so the call stack is captured at each call site rather than frozen at the wrapper's definition.\n\nThe resulting error for a caught exception will have:\n\n| Info | Field | Value |\n| --- | --- | --- |\n| `PublicErrorInfo` | `publicMessage` | `\"An unexpected error occurred\"` |\n| `PublicErrorInfo` | `code` | `\"UnhandledException\"` (customizable via `tryRailWithCode` or `tryRailWithError`) |\n| `InternalErrorInfo` | `internalMessage` | The exception message (logs only) |\n| `InternalErrorInfo` | `severity` | `Critical` |\n| `InternalErrorInfo` | `exception` | The original `SomeException` |\n| `InternalErrorInfo` | `callStack` | Haskell call chain at the `tryRail` call site |\n\n### `tryRailWithError`\n\nLike `tryRailWithCode`, but derives the error code and public message from a `HasErrorInfo` value built from the caught exception:\n\n```haskell\ntryRailWithError :: (HasCallStack, HasErrorInfo e) =\u003e (SomeException -\u003e e) -\u003e IO a -\u003e Rail a\n```\n\nThe error-building function receives the `SomeException` that was thrown, allowing the resulting error to carry information extracted from the exception itself. `errorCode` is used as the error code and `errorPublicMessage` as the public message.\n\n```haskell\n{-# LANGUAGE DeriveDataTypeable #-}\n\ndata DbError = QueryFailed Text | ConnectionLost\n  deriving (Show, Data)\n\ninstance HasErrorInfo DbError where\n  errorPublicMessage (QueryFailed _) = \"A database query failed\"\n  errorPublicMessage ConnectionLost  = \"Lost connection to the database\"\n\n-- Always map to ConnectionLost, ignoring the exception:\nsafeQuery :: Rail [Row]\nsafeQuery = tryRailWithError (\\_ -\u003e ConnectionLost) runQuery\n\n-- Inspect the exception to choose the right constructor:\nsafeQuery' :: Rail [Row]\nsafeQuery' = tryRailWithError (QueryFailed . T.pack . displayException) runQuery\n```\n\n\u003e **Note:** add `HasCallStack` to any wrapper's own signature so the call stack is captured at each call site rather than frozen at the wrapper's definition.\n\n### `UnhandledException`\n\nThe error type produced by `tryRail`. It wraps `SomeException` and implements `HasErrorInfo`, so it works anywhere a Railway error is expected:\n\n```haskell\ndata UnhandledException = UnhandledException\n  { unhandledCode      :: Maybe Text\n  , unhandledException :: SomeException\n  , unhandledCallStack :: Maybe CallStack\n  , unhandledMessage   :: Maybe Text\n  }\n```\n\nWhen produced by `tryRail`, `unhandledCode` is `Nothing` (defaulting to `\"UnhandledException\"`), `unhandledCallStack` is captured automatically at the call site, and `unhandledMessage` defaults to `Nothing` (falling back to the generic public message `\"An unexpected error occurred\"`).\n\nUse `throwUnhandledException` when you catch exceptions yourself and the default code is sufficient:\n\n```haskell\nimport qualified Control.Exception as E\n\nsafeQuery :: Rail Row\nsafeQuery = do\n  result \u003c- liftIO $ E.try runQuery\n  case result of\n    Right row -\u003e pure row\n    Left ex   -\u003e throwUnhandledException ex\n```\n\nUse `throwUnhandledExceptionWithCode` when you need a domain-specific code — it also captures the call stack automatically:\n\n```haskell\nsafeQuery :: Rail Row\nsafeQuery = do\n  result \u003c- liftIO $ E.try runQuery\n  case result of\n    Right row -\u003e pure row\n    Left ex   -\u003e throwUnhandledExceptionWithCode \"DbQueryFailed\" ex\n```\n\n### `runRail`\n\nExecutes the computation and returns `Either Failure a`:\n\n```haskell\nrunRail :: Rail a -\u003e IO (Either Failure a)\n```\n\n### `runRailT`\n\nThe general form of `runRail`, for when your base monad is not `IO`:\n\n```haskell\nrunRailT :: Monad m =\u003e RailT e m a -\u003e m (Either e a)\n```\n\nUse it when `RailT` is stacked on top of another transformer, such as `StateT` or `ReaderT`:\n\n```haskell\nimport Control.Monad.State (StateT, runStateT)\n\ndata AppState = AppState { counter :: Int }\n\ntype AppRail a = RailT Failure (StateT AppState IO) a\n\nrunAppRail :: AppState -\u003e AppRail a -\u003e IO (Either Failure a, AppState)\nrunAppRail initialState = runStateT . runRailT\n```\n\n### `HasErrorInfo`\n\nTypeclass connecting your domain error types to the standard error format. Only `errorPublicMessage` is required — all other methods have sensible defaults:\n\n```haskell\nclass HasErrorInfo e where\n  errorPublicMessage   :: e -\u003e Text                -- Required\n  errorCode            :: e -\u003e Text                -- Default: constructor name via Data\n  errorDetails         :: e -\u003e Maybe SomeErrorDetails  -- Default: Nothing\n  errorSeverity        :: e -\u003e ErrorSeverity       -- Default: Error\n  errorInternalMessage :: e -\u003e Maybe Text          -- Default: Nothing\n  errorException       :: e -\u003e Maybe SomeException -- Default: Nothing\n  errorCallStack       :: e -\u003e Maybe CallStack     -- Default: Nothing\n```\n\nUse `publicErrorInfo` and `internalErrorInfo` to assemble the corresponding records from any instance:\n\n```haskell\npublicErrorInfo  :: HasErrorInfo e =\u003e e -\u003e PublicErrorInfo\ninternalErrorInfo :: HasErrorInfo e =\u003e e -\u003e InternalErrorInfo\n```\n\n#### Simple errors — implement `errorPublicMessage` only\n\nDerive `Data` and implement `errorPublicMessage`. The `errorCode` default derives the error code from the constructor name via `Data.toConstr`:\n\n```haskell\n{-# LANGUAGE DeriveDataTypeable #-}\n\ndata OrderError = ItemOutOfStock | PaymentDeclined\n  deriving (Show, Data)\n\ninstance HasErrorInfo OrderError where\n  errorPublicMessage ItemOutOfStock  = \"One or more items are out of stock\"\n  errorPublicMessage PaymentDeclined = \"Payment was declined\"\n-- errorCode = \"ItemOutOfStock\" or \"PaymentDeclined\"\n```\n\n\u003e **Note:** the error code is the constructor name verbatim. Renaming a constructor silently changes its code, so treat constructor names as part of your public API contract.\n\n#### Full control — override individual methods\n\nOverride any combination of methods when you need custom codes, `errorDetails`, severity, or internal context. Methods you do not override keep their defaults:\n\n```haskell\ninstance HasErrorInfo OrderError where\n  errorPublicMessage ItemOutOfStock  = \"One or more items are out of stock\"\n  errorPublicMessage PaymentDeclined = \"Payment was declined\"\n\n  -- Custom codes\n  errorCode ItemOutOfStock  = \"OrderItemOutOfStock\"\n  errorCode PaymentDeclined = \"OrderPaymentDeclined\"\n\n  -- Override severity for one constructor only\n  errorSeverity PaymentDeclined = Critical\n\n  -- Override internal message for one constructor\n  errorInternalMessage PaymentDeclined = Just \"Stripe returned decline code: insufficient_funds\"\n```\n\n### `PublicErrorInfo` and `InternalErrorInfo`\n\nError data is split into two records by visibility. Use the `publicErrorInfo` and `internalErrorInfo` functions to obtain them from any `HasErrorInfo` instance.\n\n**`PublicErrorInfo`** — serialized to JSON, safe to return to callers:\n\n| JSON key | Field | `HasErrorInfo` method |\n| --- | --- | --- |\n| `message` | `publicMessage` | `errorPublicMessage` |\n| `code` | `code` | `errorCode` |\n| `details` | `details` | `errorDetails` |\n\n**`InternalErrorInfo`** — for logging and monitoring only. It implements `ToJSON` so you can log it server-side, but `SomeError`'s `ToJSON` instance only serializes `PublicErrorInfo`, so internal fields are never included in API responses:\n\n| JSON key | Field | `HasErrorInfo` method |\n| --- | --- | --- |\n| `severity` | `severity` | `errorSeverity` |\n| `message` | `internalMessage` | `errorInternalMessage` |\n| `exception` | `exception` | `errorException` |\n| `callStack` | `callStack` | `errorCallStack` |\n\n### Error Severity\n\n```haskell\ndata ErrorSeverity = Error | Critical\n```\n\nUse `Critical` for errors that need immediate attention (e.g., data corruption, infrastructure failures). Use `Error` for recoverable application-level failures.\n\n## Combining Errors from Different Sources\n\n`SomeError` is an existential wrapper, so you can mix error types freely:\n\n```haskell\ndata DbError = ConnectionFailed deriving (Show, Data)\n\ninstance HasErrorInfo DbError where\n  errorPublicMessage   ConnectionFailed = \"Service temporarily unavailable\"\n  errorCode            ConnectionFailed = \"DbConnectionFailed\"\n  errorSeverity        ConnectionFailed = Critical\n  errorInternalMessage ConnectionFailed = Just \"Postgres replica at 10.0.0.5:5432 unreachable\"\n\npipeline :: Rail ()\npipeline = do\n  validateName name \u003c!\u003e validateEmail email  -- UserError\n  fetchFromDb                                -- DbError\n```\n\n## JSON Serialization\n\n`Failure` implements `ToJSON` via `aeson`. A failed computation serializes as a JSON array of error objects. Each error is a `SomeError`, whose `ToJSON` instance delegates only to `PublicErrorInfo` — internal diagnostic fields are never included in the output:\n\n```haskell\nimport Data.Aeson (encode)\nimport qualified Data.ByteString.Lazy.Char8 as BS\n\nresult \u003c- runRail myRail\ncase result of\n  Left errors -\u003e BS.putStrLn (encode errors)\n  Right _     -\u003e pure ()\n```\n\n## License\n\n[BSD-3-Clause](LICENSE) © 2026 Ismael Carlos Velten\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fivelten%2Fmonad-rail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fivelten%2Fmonad-rail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fivelten%2Fmonad-rail/lists"}