Up to Index

Error handling with acid-state

The need for error handling

Many applications will involve transactions that can fail. My own online booking services are examples of this.

A booking must be made in a single transaction, to avoid overbooking a resource. Whether the booking is allowed to take place or not is determined by a complex set of rules, if it is not allowed this must be communicated to the user. Furthermore, the user should be informed as to why his/her booking could not be completed, so using Maybe is not an option.

In my application, and in many others, there is need for error handling.

Exceptions won't work

One way to introduce error handling is to throw exceptions inside your Querys/Updates which you then handle(catch) outside of the transactions(in snap/happstack/whatever framework). But as it turns you run into problems with complex states/transactions.

The problem is described here. In short, your exception isn't thrown when you call "update" because the state is not evaluated far enough, and the update is written to the transaction log. The exception will be thrown when the state is evaluated, by trying to write a checkpoint or doing a query on that part of the state.

So you end up with a broken state, you have to manually erase the offending transaction.

MonadError

My solution is to use MonadError:

      type MyUpdate st e a = StateT st (Either e) a

      runMyUpdate :: MyUpdate st e a -> Update st (Either e a)
      runMyUpdate act = 
        do
          s <- get
          case runStateT act s of
            Left e -> return $ Left e
	    Right (x,s') ->
              do
                put s
                return $ Right x

      setVal' :: Int -> MyUpdate Int String ()
      setVal' 5 = throwError "wrong number"
      setVal' x = put x
    

Within a MyUpdate we can use throwError/catchError and friends. If there's an uncaught error, the state won't be serialized.

We can't run makeAcidic on setVal' directly, we need to turn it into an Update first:

      setVal :: Int -> Update Int (Either String ())
      setVal = runMyUpdate . setVal'

      makeAcidic ''Int ['setVal]
    

The reason for not running runMyUpdate straight away is to let us reuse setVal' in other updates. Ideally there would also be a makeMyAcidic template-haskell function that automagically turns MyUpdate into Update.

Now we have an update that returns an Either value, in our web handler we could handle this by throwing the error as an exception:

      myUpdate :: (Exception e,
                   UpdateEvent event,
                   EventResult event ~ Either e t) =>
                   AcidState (EventState event) -> event -> IO t
      myUpdate st ev =
        do
          val <- update st ev
          case val of
            Left e -> throw e
            Right x -> return x
    

Or if we use MonadError:

      myUpdate :: (MonadError e m,
                   UpdateEvent event,
                   MonadIO m,
                   EventResult event ~ Either e t) =>
                  AcidState (EventState event) -> event -> m t
      myUpdate st ev =
        do
          val <- liftIO $ update st ev
          case val of
            Left e -> throwError e
            Right x -> return x
    

I like to use the MonadError-way, because it keeps the kind of errors that might be thrown visible in the type.

Discussion

You may use any transformer stack you want in MyUpdate. In one of my applications I add a ReaderT that carries around some often used parameters like the time of the request, the users session id and so on.

The way runMyUpdate is written the event is written to the transaction log even if it does not modify the state. This could cause your transaction logs to become needlessly large. You could throw an exception instead(it will be okay because it's not nested), but then you loose the type of the error there. I have not tried this approach in any of my projects.

jon.petter.bergman@gmail.com 2014-10-02.