Skip to content

How to handle errors at callsite vs app-wide? #340

Description

@seanhess

I've been definiing custom effects as described in Effectful.Dispatch.Dynamic

My application has a large runner which adds all the effects, then in my functions, I rely only on the effect. If I understand correctly, this is all 100% by-the-book: My app has a bunch of capabilities, and individual functions only depend on the effects they need.

  data FileSystem :: Effect where
    ReadFile  :: FilePath -> FileSystem m String

 runFileSystemIO
   :: (IOE :> es, Error FsError :> es)

runApp :: Eff (SomethingElse : FileSystem : Error FsError : es) a -> Eff es a
runApp = runErrorNoCallstackWith @FsError catchLogAndThrowError . runFileSystem . runSomethingElse

example :: FileSystem :> es => Eff es ()
example = doSomething <<= send $ ReadFile "somefile.txt

However, sometimes I need to handle errors in a handler, at the callsite where the effect is used. Perhaps I want to retry reading the file if it fails, or do something else. This doesn't seem possible. runError doesn't work, because the error effect is used inside the runner for FileSystem.

-- this does not work
example :: FileSystem :> es => Eff es ()
example = do
  res <- runErrorNoCallstack @FsError $ send $ ReadFile "somefile.txt
  case res of
    Left err -> putStrLn "do something custom with the error"
    Right contents -> putStrLn "handle file normally"

The only way I can think of to get this to work would be to re-run the file-system effect:

-- `es` contains FileSystem already
example :: (IOE :> es) => Eff es ()
example = do
  res <- runErrorNoCallstack @FsError $ runFileSystem $ send $ ReadFile "somefile.txt
  case res of
    Left err -> _
    Right contents -> _

But I have to pass IOE around everywhere now! I lose all the benefits of isolating individual effects. My handler depends on all the global effects, like IOE. It gets worse if the effect in question requires several pieces of global app config, etc.

Standard exceptions handle this use-case quite gracefully: I can catch errors globally in some app runner, but if I catch them locally, I can handle them. But an effect that can throw Exceptions doesn't immediately tell you which errors can occur, or force you to handle them.

What is the right way to do this?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions