haskell-webapps
haskell-webapps copied to clipboard
How to represent error conditions in the domain API?
Copied over from the Reddit thread
Along my crazy journey to build a "minimum viable webapp architecture", I'm stuck with a design-level issue. How do I represent error conditions/cases in my domain API?
In a bid to keep things explicit and pure [1], I designed the following first:
createTenant :: NewTenant -> AppM (Either TenantCreationError Tenant)
activateTenant :: TenantId -> AppM (Either TenantActivationError Tenant)
type AppM = ReaderT AppConfig IO
data TenantCreationError = DuplicateBackofficeDomainError Text deriving Show
data TenantActivationError = OwnerIdAbsentError deriving Show
All, was good till I had to write code dealing with createTenant
and activateTenant
, both. It turns out that Either TenantCreationError
and Either TenantActivationError
don't compose easily! For example, take a look at the type that the compiler infers for the following function:
createAndActivateTenant :: NewTenant -> AppM (Either TenantCreationError (Either TenantActivationError Tenant))
createAndActivateTenant newTenant = (createTenant newTenant) >>= \case
Left x -> return $ Left x
Right y -> do
t <- activateTenant $ (y ^. key)
return $ Right t
So, I'm left with the following options:
Option 1: Keep using Either
Continue with the current design and create even more ADTs for the Left
part of functions that compose two underlying Eithers. eg: createAndActivateTenant :: NewTenant -> Either TenantCreationOrActivationError Tenant
-- something I'm not too excited about.
Option 2: Unify all error cases
Sacrifice granularity and the self-documenting nature of the more specific type-signatures, and go with a unified ADT for all domain-level error cases, eg:
data DomainError = DuplicateBackofficeDomainError Text | OwnerIdAbsentError
createTenant :: NewTenant -> Either DomainError Tenant
activateTenant :: TenantId -> Either DomainError Tenant
Option 3: Dump Either and use MonadCatch
Not too happy with this because the error cases are not visible in the type signature at all.
Option 4: Use advanced type-level hackery
Figure out how to get something like the following:
createAndActivateTenant :: NewTenant -> Either '(Errors [TenantCreationError TenantActivationError]) Tenant
Questions
Is there any other possibility that I'm missing? Any advantage/disadvantage that I'm missing? Is there any library that implements Option 4 mentioned above?
[1] I know, it's in a ReaderT r IO
monad, so it's not "pure" in the "pure" sense, but it's "pure" in the "doesn't throw errors" sense.
Cool, I like that you make issues from it, I'm curious what you're gonna choose in the end!
@BartAdv I'll settle for Option 2 (unify into a single ADT) or Option 3 (throw exceptions instead) and see how the domain API gets used in the servant handler. If it doesn't come out well, I'm expected Haskell to help me with the refactor.
@saurabhnanda I like the way Servant handles errors and its very flexible!
https://hackage.haskell.org/package/servant-server-0.9/docs/src/Servant-Server-Internal-ServantErr.html
data ServantErr = ServantErr { errHTTPCode :: Int
, errReasonPhrase :: String
, errBody :: LBS.ByteString
, errHeaders :: [HTTP.Header]
} deriving (Show, Eq, Read, Typeable)
and when raising errors, you can have specific errors... like err404
-- | 'err400' Bad Request
--
-- Example:
--
-- > failingHandler :: Handler ()
-- > failingHandler = throwError $ err400 { errBody = "Your request makes no sense to me." }
--
err404 :: ServantErr
err404 = ServantErr { errHTTPCode = 404
, errReasonPhrase = "Not Found"
, errBody = ""
, errHeaders = []
}
and uses Handler
type Handler = ExceptT ServantErr IO
I believe rather than using ADT a record would make better sense?
What do you think?
there are at least 2 classes of errors, 1, bad data, likerequest can't be parsed, product id is invalid or something. For public consumption, i'd go with 400 or 404. since the only user of the api is the UI, it might make sense to return some json that indicates what's wrong more precisely. - my preference is still 400 - bad request the other class of errors, something internal is broken, like the database is down, or unreadable, or a logfile can't be written, system can't run type stuff. i like health checks. easy to monitor.