Handling `IO Bool` and `IO (Either a b)` inside `IO (Maybe b)` {-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE ScopedTypeVariables #-}

data E a
  = E1
  | E2
  | E3 a

data L
data R

f :: IO (Maybe (E L))
f =
  (undefined :: IO Bool) &gt;&gt;= \case
    False -&gt; return (Just E1)
    True  -&gt;
      (undefined :: IO Bool) &gt;&gt;= \case
        False -&gt; return (Just E2)
        True  -&gt;
          (undefined :: IO (Either L R)) &gt;&gt;= \case
            Left e  -&gt; return (Just (E3 e))
            Right v -&gt; (undefined :: R -&gt; IO ()) v &gt;&gt; return Nothing


How to get rid of these three nested cases?
Since you need to abort your computation early and return a value different from the final output, the ExceptT transformer would be useful.

import Control.Monad (unless)
import Control.Monad.IO.Class
import Control.Monad.Trans.Except

data E a
  = E1
  | E2
  | E3 a

data L
data R

f :: ExceptT (E L) IO ()
f = do
  liftIO (undefined :: IO Bool) &gt;&gt;= flip unless (throwE E1)
  liftIO (undefined :: IO Bool) &gt;&gt;= flip unless (throwE E2)
  liftIO (undefined :: IO (Either L R)) &gt;&gt;=
    either (throwE . E3) (liftIO . (undefined :: R -&gt; IO ()))


If we want, we can then convert ExceptT (E L) IO () to IO (Maybe (E L)):

f' :: IO (Maybe (E L))
f' = either Just (const Nothing) &lt;$&gt; runExceptT f


If could be further polished by using helper functions from Control.Conditional or a similar library, and by extending the functions, for which undefined fills in, to work within any MonadIO (if that's possible).