Recovering from Errors in the State monad

January 31, 2023

I recently encountered an issue where we needed to log information about a failed task to the database but this information is accumulated over the life of the task. At first I thought that layering in the State monad would solve our issue.

The problem with the State monad in this case is that when an error occurs it’s harder to recover the state. The State monad should be used with an immutable state which is “copied” each time it changes, but if an error occurs you can’t really peek to see what it was at the time of failure.

The result of this is -

ERROR: java.lang.RuntimeException: boom

As you can see, in the throwable case, we don’t have access to MyState. So if an error occurs, we’re stuck out.

One step in the right direction could be to using EitherT around StateT so the state can be recovered -

However, the problem here is that the RuntimeException gets thrown from inside the Task and does not propagate to the EitherT, so we still lose the state!

java.lang.RuntimeException: boom
  at $anonfun$statefulTask$5(<console>:6)
  ... 16 elided

To get around this, we MUST ensure that any Task that gets executed uses .attempt so its exceptions are captured by the EitherT -

Then we get the following -

STATE: MyState(3)
ERROR: java.lang.RuntimeException: boom

However, this is extremely easy to mess up. If you have one stray task where you haven’t totally shaken the error out, you’ll lose your state. This is precisely what happened in the prior example. Any Task that is to be eventually turned into a StatefulTask at some point needs to wrap it in our special liftTask function. It requires extreme programmer diligence and thus is something that will inevitably go wrong.

For this reason many in the Haskell world prefer using ReaderT over an IORef which contains a mutable reference. That’s essentially what we’re doing in this next example, except in Scala we don’t need IORef and can just rely on mutable fields.

And the output -

STATE: MyState(3)
ERROR: java.lang.RuntimeException: boom

No need for any special error handling required by those constructing tasks, stateful or otherwise; any Task can be properly handled this way at the very end when we actually run our StatefulTask.