monad-rail-0.1.0.0: Railway-oriented error handling for Haskell
Safe HaskellNone
LanguageHaskell2010

Monad.Rail

Description

Railway-Oriented error handling for Haskell applications.

Monad.Rail is a Haskell library implementing Railway-Oriented Programming (ROP), a functional approach to error handling that makes both success and failure paths explicit and composable.

What is Railway-Oriented Programming?

Railway-Oriented Programming uses a railway analogy: your program has two tracks, one for success and one for failure. Operations can move between tracks, and once on the failure track, you stay there until the end.

This library implements ROP through:

  • RailT - A monad transformer for railway computations
  • throwError - Moving to the failure track
  • (<!>) - Combining validations while collecting all errors
  • Error accumulation - Multiple errors are gathered, not just the first one

Quick Start

Implement HasErrorInfo with errorPublicMessage — the only required method. Derive Data to get an automatic error code from the constructor name:

>>> {-# LANGUAGE DeriveDataTypeable #-}
>>> 
>>> data UserError = NameEmpty | EmailInvalid
>>>   deriving (Show, Data)
>>> 
>>> instance HasErrorInfo UserError where
>>>   errorPublicMessage NameEmpty    = "Name cannot be empty"
>>>   errorPublicMessage EmailInvalid = "Email format is invalid"

Override individual methods when you need custom codes or errorDetails:

>>> instance HasErrorInfo UserError where
>>>   errorPublicMessage NameEmpty    = "Name cannot be empty"
>>>   errorPublicMessage EmailInvalid = "Email format is invalid"
>>> 
>>>   errorCode NameEmpty    = "UserNameEmpty"
>>>   errorCode EmailInvalid = "UserEmailInvalid"

Use in your railway:

>>> validateUser :: Rail ()
>>> validateUser = do
>>>   checkName <!> checkEmail
>>>   saveToDatabase
>>>   liftIO $ putStrLn "User created!"

Run and handle the result:

>>> main :: IO ()
>>> main = do
>>>   result <- runRail validateUser
>>>   case result of
>>>     Right () -> putStrLn "Success!"
>>>     Left errors -> print errors

Error Accumulation

The <!> operator is key to ROP. It runs both validations regardless of failure:

>>> validate :: Rail ()
>>> validate = do
>>>   checkName <!> checkEmail <!> checkAge
>>>   -- If any checks fail, ALL errors are reported together

Core Concepts

  • Rail - The monad for your computations
  • Failure - Contains accumulated errors
  • SomeError - Wrapper for any error type
  • HasErrorInfo - Typeclass for custom errors
  • <!> - Error accumulation operator

Logging and Monitoring

Each error carries two separate records:

  • PublicErrorInfo - Safe for end users: message, code, and details. Assembled via publicErrorInfo; serialized to JSON in API responses. Null fields are omitted.
  • InternalErrorInfo - Sensitive diagnostics: severity, internal message, exception, and call stack. Assembled via internalErrorInfo; implements ToJSON for structured log output but is never included in public API responses. Null fields are omitted.

The Failure type implements ToJSON, so errors serialize automatically:

>>> import Data.Aeson (encode)
>>> result <- runRail myRail
>>> case result of
>>>   Left errors -> BS.putStrLn $ encode errors
>>>   Right _ -> pure ()
Synopsis

Core Types

newtype RailT e (m :: Type -> Type) a Source #

The Railway-Oriented monad transformer.

RailT wraps ExceptT to provide composable error handling with support for error accumulation. It is parameterized over:

  • e - The error type (typically Failure)
  • m - The underlying monad (often IO)
  • a - The type of successful values

The transformer implements Functor, Applicative, Monad, and MonadIO, allowing it to be used seamlessly with do-notation and other monadic operations.

Examples

Create a computation that can fail:

>>> computation :: RailT Failure IO String
>>> computation = do
>>>   liftIO $ putStrLn "Starting..."
>>>   pure "result"

Use with Rail for a more concise type signature:

>>> computation :: Rail String
>>> computation = do
>>>   liftIO $ putStrLn "Starting..."
>>>   pure "result"

Constructors

RailT 

Fields

Instances

Instances details
MonadIO m => MonadIO (RailT e m) Source # 
Instance details

Defined in Monad.Rail.Types

Methods

liftIO :: IO a -> RailT e m a #

Monad m => Applicative (RailT e m) Source # 
Instance details

Defined in Monad.Rail.Types

Methods

pure :: a -> RailT e m a #

(<*>) :: RailT e m (a -> b) -> RailT e m a -> RailT e m b #

liftA2 :: (a -> b -> c) -> RailT e m a -> RailT e m b -> RailT e m c #

(*>) :: RailT e m a -> RailT e m b -> RailT e m b #

(<*) :: RailT e m a -> RailT e m b -> RailT e m a #

Functor m => Functor (RailT e m) Source # 
Instance details

Defined in Monad.Rail.Types

Methods

fmap :: (a -> b) -> RailT e m a -> RailT e m b #

(<$) :: a -> RailT e m b -> RailT e m a #

Monad m => Monad (RailT e m) Source # 
Instance details

Defined in Monad.Rail.Types

Methods

(>>=) :: RailT e m a -> (a -> RailT e m b) -> RailT e m b #

(>>) :: RailT e m a -> RailT e m b -> RailT e m b #

return :: a -> RailT e m a #

type Rail a = RailT Failure IO a Source #

A convenient type alias for Railway computations in the IO monad.

This is the most common way to use RailT. It fixes the error type to Failure and the base monad to IO, providing a simple interface for building IO-based applications with Railway-Oriented error handling.

Rail a is equivalent to RailT Failure IO a

Example

>>> myApp :: Rail ()
>>> myApp = do
>>>   validateInput
>>>   processData
>>>   liftIO $ putStrLn "Complete!"

newtype Failure Source #

Represents a collection of one or more application errors accumulated during a Railway computation.

This type is used as the error type in RailT computations. It guarantees that at least one error is always present (using NonEmpty), which is essential for the Railway-Oriented programming model where a failure state must contain error information.

Multiple errors are accumulated when using the <!> operator for validations, allowing you to collect all validation errors before reporting failure.

Combining Errors

Use the Semigroup instance to combine multiple Failure values:

>>> let err1 = Failure (SomeError e1 :| [])
>>> let err2 = Failure (SomeError e2 :| [])
>>> let combined = err1 <> err2  -- Contains both errors

Constructors

Failure 

Fields

  • getErrors :: NonEmpty SomeError

    Extracts the non-empty list of errors.

    Use this function to access the individual errors for logging, reporting, or further processing.

Instances

Instances details
ToJSON Failure Source # 
Instance details

Defined in Monad.Rail.Error

Semigroup Failure Source # 
Instance details

Defined in Monad.Rail.Error

Show Failure Source # 
Instance details

Defined in Monad.Rail.Error

Running Railways

runRailT :: Monad m => RailT e m a -> m (Either e a) Source #

Runs a RailT computation in any base monad m, returning m (Either e a).

This is the general form of runRail. Use it when your base monad is not IO — for example, when RailT is stacked on top of StateT, ReaderT, or any other transformer.

Example: custom monad stack

>>> import Control.Monad.State (StateT, runStateT)
>>> 
>>> type AppRail a = RailT Failure (StateT AppState IO) a
>>> 
>>> runAppRail :: AppState -> AppRail a -> IO (Either Failure a, AppState)
>>> runAppRail initialState = runStateT . runRailT

runRail :: Rail a -> IO (Either Failure a) Source #

Runs a Rail computation and returns the result or accumulated errors.

This function executes the railway computation and returns the final result wrapped in Either. On success, you get Right value. On failure, you get Left errors containing all accumulated SomeError values.

The returned Failure contains at least one error (guaranteed by NonEmpty), making it safe to handle errors without null checks.

Example

>>> result <- runRail myComputation
>>> case result of
>>>   Right value -> putStrLn $ "Success: " ++ show value
>>>   Left errors -> do
>>>     putStrLn "Errors occurred:"
>>>     mapM_ print (getErrors errors)

JSON Serialization

The Failure type implements ToJSON, so you can easily serialize errors:

>>> import Data.Aeson (encode)
>>> result <- runRail myComputation
>>> case result of
>>>   Left errors -> putStrLn $ BS.unpack $ encode errors
>>>   Right _ -> pure ()

Throwing Errors

throwError :: forall e (m :: Type -> Type) a. (HasErrorInfo e, Show e, Typeable e, Monad m) => e -> RailT Failure m a Source #

Throws an application error in the Railway.

This function wraps the error in SomeError and then in a Failure container, immediately failing the computation with that error. Subsequent operations in the do-block will not be executed.

Any error type with HasErrorInfo, Show, and Typeable constraints can be thrown directly — no need to wrap it in SomeError manually.

Use this function when you encounter an error condition that should stop execution. For validations where you want to collect multiple errors before failing, use the <!> operator instead.

Example

>>> checkAge :: Int -> Rail ()
>>> checkAge age = do
>>>   when (age < 0) $ throwError AgeNegative
>>>   pure ()

Error Accumulation

When multiple errors occur (e.g., with <!>), Failure will contain all of them. You can then handle them together or individually:

>>> result <- runRail (checkName <!> checkEmail)
>>> case result of
>>>   Left errors -> mapM_ print (getErrors errors)
>>>   Right () -> putStrLn "All valid!"

throwUnhandledException :: forall (m :: Type -> Type) a. (HasCallStack, Monad m) => SomeException -> RailT Failure m a Source #

Throw an unhandled IO exception as a Railway error using the default code "UnhandledException".

This is a convenience wrapper around throwError for the common pattern of catching an IO exception and re-throwing it as an UnhandledException. It captures the call stack automatically, so you do not need to pass it manually.

The call stack is captured at the call site of throwUnhandledException, not at the definition of any wrapper around it (provided the wrapper also carries HasCallStack).

Use throwUnhandledExceptionWithCode when you need a domain-specific error code.

Example

>>> import qualified Control.Exception as E
>>> 
>>> safeQuery :: Rail Row
>>> safeQuery = do
>>>   result <- liftIO $ E.try runQuery
>>>   case result of
>>>     Right row -> pure row
>>>     Left ex   -> throwUnhandledException ex

throwUnhandledExceptionWithCode :: forall (m :: Type -> Type) a. (HasCallStack, Monad m) => Text -> SomeException -> RailT Failure m a Source #

Like throwUnhandledException, but with a domain-specific error code.

The call stack is captured at the call site, provided the wrapper also carries HasCallStack.

Example

>>> import qualified Control.Exception as E
>>> 
>>> safeQuery :: Rail Row
>>> safeQuery = do
>>>   result <- liftIO $ E.try runQuery
>>>   case result of
>>>     Right row -> pure row
>>>     Left ex   -> throwUnhandledExceptionWithCode "DbQueryFailed" ex

Exception handling

tryRail :: HasCallStack => IO a -> Rail a Source #

Safely execute an IO action that may throw exceptions, converting any exception into a Railway error.

This is a convenience wrapper around tryRailWithCode that uses the default error code "UnhandledException". Use tryRailWithCode for a custom code with the same generic message, or tryRailWithError to also customise the public message.

Example

>>> readConfig :: FilePath -> Rail String
>>> readConfig path = tryRail (readFile path)
>>> 
>>> pipeline :: FilePath -> Rail ()
>>> pipeline filePath = do
>>>   content <- tryRail (readFile filePath)
>>>   validateName content <!> validateEmail content
>>>   saveToDb content

tryRailWithCode :: HasCallStack => (SomeException -> Text) -> IO a -> Rail a Source #

Like tryRail, but with a custom error code derived from the exception.

Wraps an IO action that may throw, converting any exception into a Railway error whose code is produced by applying the given function to the caught exception. The public message remains the generic default ("An unexpected error occurred"). Use tryRailWithError when you also need a domain-specific public message.

Pass a constant function (const "MyCode") when the code is fixed, or inspect the exception to return different codes:

>>> tryDb :: HasCallStack => IO a -> Rail a
>>> tryDb = tryRailWithCode (const "DbError")
>>> 
>>> tryHttp :: HasCallStack => IO a -> Rail a
>>> tryHttp = tryRailWithCode $ \ex ->
>>>   if "timeout" `T.isInfixOf` T.pack (Ex.displayException ex)
>>>     then "HttpTimeout"
>>>     else "HttpError"

Note: if you partially apply this function, add HasCallStack to the wrapper's own signature so the call stack is captured at each call site rather than frozen at the definition of the wrapper.

tryRailWithError :: (HasCallStack, HasErrorInfo e) => (SomeException -> e) -> IO a -> Rail a Source #

Like tryRailWithCode, but derives the error code and public message from a HasErrorInfo value built from the caught exception.

The error-building function receives the SomeException that was thrown, allowing the resulting error value to carry information extracted from the exception itself. The HasErrorInfo instance then supplies errorCode as the error code and errorPublicMessage as the public message.

Example

>>> {-# LANGUAGE DeriveDataTypeable #-}
>>> 
>>> data DbError = QueryFailed Text | ConnectionLost
>>>   deriving (Show, Data)
>>> 
>>> instance HasErrorInfo DbError where
>>>   errorPublicMessage (QueryFailed _) = "A database query failed"
>>>   errorPublicMessage ConnectionLost  = "Lost connection to the database"
>>> 
>>> safeQuery :: Rail [Row]
>>> safeQuery = tryRailWithError (\_ -> ConnectionLost) runQuery
>>> 
>>> -- Inspect the exception to choose the right constructor:
>>> safeQuery' :: Rail [Row]
>>> safeQuery' = tryRailWithError (QueryFailed . T.pack . Ex.displayException) runQuery

Note: add HasCallStack to any wrapper so the call stack is captured at each call site rather than frozen at the wrapper's definition.

Operators

(<!>) :: forall (m :: Type -> Type). Monad m => RailT Failure m () -> RailT Failure m () -> RailT Failure m () infixl 5 Source #

Accumulates errors from two Railway validations.

This operator runs both validations regardless of whether the first one fails, collecting all errors before failing. This is the key operator for implementing Railway-Oriented Programming in Haskell.

The operator is left-associative ('infixl') with precedence 5, meaning:

v1 <!> v2 <!> v3

is interpreted as:

(v1 <!> v2) <!> v3

Example: Multiple Validations

Collect all validation errors at once:

>>> validateUser :: Rail ()
>>> validateUser = do
>>>   validateName <!> validateEmail <!> validateAge
>>>   saveToDatabase
>>>   liftIO $ putStrLn "User created successfully!"

If all validations pass, execution continues to saveToDatabase. If any validation fails, all errors are accumulated and the computation stops.

Behavior

The behavior depends on the results of both validations:

  • Both succeed (Right, Right) → Continue execution
  • First fails, second succeeds (Left, Right) → Stop with first error
  • First succeeds, second fails (Right, Left) → Stop with second error
  • Both fail (Left, Left) → Stop with combined errors (using <>)

This allows Railway-Oriented Programming where you can express both paths (success and failure) explicitly in your code.

Use Cases

The <!> operator is ideal for:

  • Form validation (check multiple fields, report all errors)
  • Configuration validation (validate all settings, report all problems)
  • Data pipeline validation (check all constraints, fail with all violations)
  • Any scenario where you want "fail-fast" with "report-all-errors"

Error types

data ErrorSeverity Source #

Represents the severity level of an application error.

Severity levels are used to categorize errors by their importance and urgency. This information is useful for logging, monitoring, and error handling strategies.

Constructors

Error

Indicates a standard error that occurred during the execution of a Railway. Standard errors are recoverable and do not require immediate attention.

Critical

Indicates a critical error that occurred during the execution of a Railway. Critical errors may require immediate attention and indicate serious problems that could affect system stability.

Instances

Instances details
ToJSON ErrorSeverity Source # 
Instance details

Defined in Monad.Rail.Error

Enum ErrorSeverity Source # 
Instance details

Defined in Monad.Rail.Error

Show ErrorSeverity Source # 
Instance details

Defined in Monad.Rail.Error

Eq ErrorSeverity Source # 
Instance details

Defined in Monad.Rail.Error

Ord ErrorSeverity Source # 
Instance details

Defined in Monad.Rail.Error

data SomeErrorDetails Source #

An existential wrapper that can hold any value with ToJSON, Show, and Typeable constraints.

This allows errorDetails and PublicErrorInfo to carry structured detail values of any type, deferring JSON serialization until needed while preserving the ability to recover the concrete type via cast.

Example:

>>> SomeErrorDetails ("usr_123" :: Text)
>>> SomeErrorDetails (42 :: Int)
>>> SomeErrorDetails (object ["field" .= ("email" :: Text)])

Constructors

(ToJSON a, Show a, Typeable a) => SomeErrorDetails a 

data PublicErrorInfo Source #

Contains the public-facing information about an application error.

All fields in this record are safe to expose to end users and will be included in JSON serialization. This record is the only part of an error that flows into API responses.

See InternalErrorInfo for the complementary record holding sensitive diagnostic data.

Constructors

PublicErrorInfo 

Fields

  • publicMessage :: Text

    A human-readable message for end users. This message should be clear, helpful, and safe to display to clients. It must not contain sensitive information such as database connection details, internal IP addresses, or stack traces.

    Example: "Invalid email format"

  • code :: Text

    A machine-readable error code that categorizes the type of error.

    Error codes are useful for:

    • Logging and monitoring systems
    • Creating error statistics and metrics
    • Routing errors to appropriate handlers
    • Building error catalogs

    Example codes: "UserNameEmpty", "DbConnectionFailed"

  • details :: Maybe SomeErrorDetails

    Optional context details associated with the error.

    This field can hold any value with ToJSON, Show, and Typeable constraints, providing additional context about the error that is safe to share with the caller. For example:

    • Affected resource identifiers
    • Custom business logic data

    The existential SomeErrorDetails wrapper preserves the original value type, allowing downstream code to recover the concrete type via cast while still supporting JSON serialization.

    Example: Just (SomeErrorDetails (object ["resourceId" .= ("usr_123" :: Text)]))

data InternalErrorInfo Source #

Contains internal diagnostic information about an application error.

This record implements ToJSON so it can be serialized for server-side logging and monitoring. However, SomeError's ToJSON instance delegates only to PublicErrorInfo, so none of these fields ever appear in public API responses.

See PublicErrorInfo for the complementary record holding user-facing data.

Constructors

InternalErrorInfo 

Fields

  • internalMessage :: Maybe Text

    An optional technical message for administrators and logs. This message can contain sensitive infrastructure details, stack traces, database information, and other diagnostic data.

    Nothing means the public message is sufficient for diagnostic purposes.

    Example: Just "Failed to connect to replica database at 192.168.1.5:5432"

  • severity :: ErrorSeverity

    The severity level of the error, indicating how critical it is. See ErrorSeverity for available levels.

  • exception :: Maybe SomeException

    An optional runtime exception associated with the error, if any.

    This field is useful for capturing the underlying exception that caused the error, such as a database connection timeout or file I/O error. It is intended for logging and debugging purposes only.

  • callStack :: Maybe CallStack

    The Haskell call stack at the point the error was constructed.

    Populate this field by adding a HasCallStack constraint to the function that builds the error and passing callStack. Serialized as a human-readable string via prettyCallStack.

    Note: the field name callStack shadows the callStack implicit-parameter accessor when both are in scope. Qualify the latter as GHC.Stack.callStack to avoid ambiguity.

class HasErrorInfo e where Source #

A type class for converting custom error types into serializable error information.

Implement errorPublicMessage — the only required method — to integrate any error type with the Railway error system. All other methods have defaults and can be overridden individually as needed.

Use publicErrorInfo and internalErrorInfo to assemble the corresponding records from an instance.

Simple errors: implement errorPublicMessage only

Derive Data and implement errorPublicMessage. The errorCode default derives the error code from the constructor name via toConstr:

>>> {-# LANGUAGE DeriveDataTypeable #-}
>>> 
>>> data UserError = NameEmpty | EmailInvalid
>>>   deriving (Show, Data)
>>> 
>>> instance HasErrorInfo UserError where
>>>   errorPublicMessage NameEmpty    = "Name cannot be empty"
>>>   errorPublicMessage EmailInvalid = "Email format is invalid"
>>> -- publicErrorInfo NameEmpty
>>> --   = PublicErrorInfo { publicMessage = "Name cannot be empty"
>>> --                     , code          = "NameEmpty"
>>> --                     , details       = Nothing }

Full control: override any field method

Override individual methods when you need custom codes, details, or internal context. Methods you do not override keep their defaults:

>>> instance HasErrorInfo UserError where
>>>   errorPublicMessage NameEmpty    = "Name cannot be empty"
>>>   errorPublicMessage EmailInvalid = "Email format is invalid"
>>> 
>>>   errorCode NameEmpty    = "UserNameEmpty"
>>>   errorCode EmailInvalid = "UserEmailInvalid"
>>> 
>>>   errorSeverity _               = Critical
>>>   errorInternalMessage NameEmpty = Just "name field was empty string after trimming"

Note on errorCallStack and callStack

The assembled InternalErrorInfo record has a field named callStack. If both InternalErrorInfo and Stack are imported unqualified, the name callStack may be ambiguous. Qualify callStack to avoid ambiguity.

Minimal complete definition

errorPublicMessage

Methods

errorPublicMessage :: e -> Text Source #

A human-readable message safe to display to end users. This is the only required method.

errorCode :: e -> Text Source #

A machine-readable error code. Defaults to the constructor name via toConstr.

Override when you need a code that differs from the constructor name.

Example: errorCode NameEmpty = "UserNameEmpty"

default errorCode :: Data e => e -> Text Source #

errorDetails :: e -> Maybe SomeErrorDetails Source #

Optional details safe to share with callers. Defaults to Nothing.

Wrap your value with SomeErrorDetails to store it:

>>> errorDetails MyError = Just (SomeErrorDetails (object ["field" .= ("email" :: Text)]))

errorSeverity :: e -> ErrorSeverity Source #

Severity level of the error. Defaults to Error.

errorInternalMessage :: e -> Maybe Text Source #

An optional technical message for logs, safe to contain sensitive details. Defaults to Nothing.

errorException :: e -> Maybe SomeException Source #

An optional underlying runtime exception. Defaults to Nothing.

errorCallStack :: e -> Maybe CallStack Source #

The Haskell call stack at the point the error was constructed. Defaults to Nothing.

Populate by adding HasCallStack to the function that builds the error and passing Just GHC.Stack.callStack.

publicErrorInfo :: HasErrorInfo e => e -> PublicErrorInfo Source #

Assembles a PublicErrorInfo from a HasErrorInfo instance.

This is the canonical way to obtain the public-facing error record for logging or serialization. The result contains only data safe to expose to end users.

internalErrorInfo :: HasErrorInfo e => e -> InternalErrorInfo Source #

Assembles an InternalErrorInfo from a HasErrorInfo instance.

This is the canonical way to obtain the internal diagnostic record for server-side logging and monitoring. Never include this in API responses.

data SomeError Source #

A wrapper type that can hold any application error implementing HasErrorInfo.

This existential type allows you to combine errors of different types in the same Railway computation. It uses existential quantification to hide the concrete error type while preserving the ability to extract error information via the HasErrorInfo interface. The Typeable constraint allows recovery of the original error type via cast when needed.

This is particularly useful when you have multiple error sources (e.g., validation errors, database errors, network errors) and want to combine them in a single computation.

JSON serialization

SomeError's ToJSON instance serializes only the PublicErrorInfo fields. InternalErrorInfo is intentionally excluded so that sensitive diagnostic data (internal messages, call stacks, exceptions) is never accidentally exposed in API responses. Use internalErrorInfo directly if you need to serialize that data for server-side logging.

Example

>>> data UserError = NameEmpty
>>> data DatabaseError = ConnectionFailed
>>> 
>>> instance HasErrorInfo UserError where { ... }
>>> instance HasErrorInfo DatabaseError where { ... }
>>> 
>>> validate :: Rail ()
>>> validate = do
>>>   throwError NameEmpty         -- User error
>>>   throwError ConnectionFailed -- Database error

Constructors

(HasErrorInfo e, Show e, Typeable e) => SomeError e 

data UnhandledException Source #

Wrapper for unhandled exceptions that can be used as an error type.

This type captures a SomeException thrown in IO and makes it compatible with the Railway error system via its HasErrorInfo instance. It is the error type produced by tryRail when an IO action throws.

The publicMessage of the assembled PublicErrorInfo is intentionally generic so that internal details are never accidentally exposed to end users. The original exception is stored in the exception field of InternalErrorInfo for logging and debugging.

unhandledCode lets you assign a domain-specific error code when you catch exceptions manually, rather than relying on the default "UnhandledException":

>>> import qualified Control.Exception as E
>>> 
>>> safeQuery :: Rail Row
>>> safeQuery = do
>>>   result <- liftIO $ E.try runQuery
>>>   case result of
>>>     Right row -> pure row
>>>     Left ex   -> throwUnhandledExceptionWithCode "DbQueryFailed" ex

Or use throwUnhandledException when the default code is sufficient:

>>> safeQuery :: Rail Row
>>> safeQuery = do
>>>   result <- liftIO $ E.try runQuery
>>>   case result of
>>>     Right row -> pure row
>>>     Left ex   -> throwUnhandledException ex

When using tryRail, the code defaults to "UnhandledException" and the call stack is captured automatically at the call site.

Constructors

UnhandledException 

Fields