module ErrorsGuide where

import Control.Monad.Except (MonadError (..))
import Control.Monad.Trans
import Data.Text (Text)
import Database.PostgreSQL.Entity (Entity, selectById)
import Database.PostgreSQL.Entity.Types (GenericEntity)
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Transact (DBT)
import GHC.Generics (Generic)

Error Handling

Error handling is a tricky subject, and most often you will have to provide translation layers between your different components to express how a request has failed and what is the relevant information to be reported.

For example, inserting twice the same entity with the same primary key will raise an error in the database engine that you have violated the uniqueness constraint of a primary key. This is of little use for consumers of the system, who simply need to be told that their chosen email address or username is already used.

Building a top-down error datatype can be a very good or very bad idea, and should sometimes be replaced with a more extensible mechanism like Haskell Exceptions (whose datatypes can be used outside of this mechanism, fortunately).

Let us consider a simple usecase, where we wish to express the following error modes:

  • Entity was not found

  • Entity is in a Bad State™

  • Entity processing is running

data EntityError
  = EntityNotFound
  | EntityBadState
  | EntityProcessingIsRunning
  deriving (Eq, Show)

A MonadError stack can be used to handle errors with your data-type.

Here are the functions that we will be using:

This function allows great control over the way we report errors, and allows us to plug a MonadError for reporting.

withPool :: (MonadBaseControl IO m)
         => Pool Connection
         -> DBT m a
         -> m a

Those two functions show that we do not have to put a MonadError everywhere, and if a lower-level error happens, we can let it bubble up to create a higher-level error (like status code 500 in an http server).

data E = E
  { eId :: Text
  }
  deriving stock (Eq, Show, Generic)
  deriving
    (Entity)
    via (GenericEntity '[]) E
  deriving anyclass (FromRow)
insertEntity :: (MonadIO m)
             => E
             -> DBT m ()
getEntity
  :: (MonadError EntityError m, MonadIO m)
  => Int
  -> DBT m E
getEntity key = do
  result <- selectById (Only key)
  case result of
    Just e -> pure e
    Nothing -> lift $ throwError EntityNotFound

↑ Here, we convert a valid database response into a more precise business logic component.