{"id":17537527,"url":"https://github.com/peterbecich/on-error","last_synced_at":"2026-01-07T14:37:16.166Z","repository":{"id":84146596,"uuid":"302489526","full_name":"peterbecich/on-error","owner":"peterbecich","description":"Clearly-delineated error-handling","archived":false,"fork":false,"pushed_at":"2020-10-09T00:09:24.000Z","size":17,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-12-09T01:21:25.276Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"Simspace/on-error","license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peterbecich.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-09T00:05:58.000Z","updated_at":"2020-10-09T00:06:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"ce2beeb0-eedc-4576-9ffb-d4b7ae0f1925","html_url":"https://github.com/peterbecich/on-error","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterbecich%2Fon-error","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterbecich%2Fon-error/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterbecich%2Fon-error/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterbecich%2Fon-error/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peterbecich","download_url":"https://codeload.github.com/peterbecich/on-error/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246135739,"owners_count":20729056,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-20T20:42:07.283Z","updated_at":"2026-01-07T14:37:16.139Z","avatar_url":"https://github.com/peterbecich.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# on-error: clearly-delineated error-handling\n\nError-handling code is messy, but if we want to provide good error messages, we should still take the time to do it\nright. `on-error` provides a naming convention which clearly distinguishes error-handling code from the rest of the\ncode, thereby allowing developers to only pay attention to the error-handling code when they want to do so.\n\nThe naming convention is as follows:\n\n1. `on[Condition]ThrowError` functions detect errors\n2. `[transform]Error` functions transform errors\n3. `onError[Action]` functions handle errors\n\nFurthermore, those functions are designed to be composed using `(.)` into a clearly-delineated block of error-handling\ncode. Here is an example:\n\n    countDogs :: Response Int\n    countDogs = onErrorCatch sendErrorStatus\n              . onNothingThrowError 500\n           =\u003c\u003c$ liftIO\n              $ fetchCount \"http://example.com/dogs/count\"\n\n    fetchCount :: Text -\u003e IO (Maybe Int)\n    sendErrorStatus :: Int -\u003e Response a\n\nEverything before the `(=\u003c\u003c$)` (which is just a low-precedence version of `(=\u003c\u003c)`) is error-handling code, and\neverything after it is normal code. Once our brains have learned to recognize those error-handling blocks as such, we\ncan take a brief look at the above definition and quickly home in to the important part: `countDogs` calls `fetchCount`\nat a specific URL, while the rest of the code embeds this `IO` computation into a `Response` computation, and deals with\nthe error cases somehow. This is often a good enough level of understanding, but if later on we do need to understand\nthe error-handling code, we can take a closer look and determine that the `Nothing` case is handled by calling\n`sendErrorStatus 500`.\n\nWithout `on-error`, the implementation of `countDogs` might look something like this:\n\n    countDogs' :: Response Int\n    countDogs' = do\n      r \u003c- liftIO $ fetchCount \"http://example.com/dogs/count\"\n      case r of\n        Nothing -\u003e sendErrorStatus 500\n        Just count -\u003e pure count\n\nOr like this:\n\n    countDogs'' :: Response Int\n    countDogs'' = join\n                . fmap (maybe (sendErrorStatus 500) pure)\n                . liftIO\n                $ fetchCount \"http://example.com/dogs/count\"\n\n`countDogs'` is quite clear, but you only realize that the second part of the function only performs error-handling\nafter you have already read and understood that part. `countDogs''` uses a different style in which the core\n`fetchCount` computation is gradually transformed into a `Response` computation, but again, it's unclear whether some of\nthose transformations affect the happy path or just the error cases until after you've read and understood those\ntransformations.\n\n\n## Detecting errors\n\nFunctions which can fail do so in one of three ways:\n\n1. By returning a special value such as `Nothing` or `Either`.\n2. By signaling failure using some monadic effect, such `ExceptT.throwE`, `MonadError.throwError`, or `MonadFail.fail`.\n3. By throwing an exception.\n\nIn each case, we want to detect the error condition and to convert it to a value of some type `e` representing the\nerrors which we know can happen within the current code. If you only plan to log the error or to display it to the user,\n`Text` is a good enough representation, but if you plan to handle some of those errors later on, `SomeException` and\n`Text` are terrible representations because they don't give your callers any information about the set of error cases\nthey might want to handle. If you want to do error-_handling_, not just error-displaying, a sum type would be a better\nchoice for `e`. See [Handling errors](#handling-errors) for some concrete suggestions.\n\nIn any case, here's how to obtain an `e` in all three cases.\n\n1.  For `Nothing`, use `onNothingThrowError` with a value of type `e` to be thrown if the value is `Nothing`.\n\n    For `Left x`, use `onLeftThrowError`. It uses `x` as the error, which you can then convert to an `e` using\n    `mapError`. For other, less common values, define a custom `on[Condition]ThrowError` function in order to avoid\n    polluting your non-error-handling code with error-handling concerns such as converting to `Maybe` or `Either`.\n2.  For `ExceptT.throwE` and `MonadError.throwError`, there is nothing to do, because `Control.Monad.Trans.OnError`\n    already uses `ExceptT` to propagate the error upwards. The `Control.Monad.OnError` API is slightly different in that\n    regard, see the [Propagating and transforming errors](#propagating-and-transforming-errors) section for details.\n\n    For `MonadFail.fail`, the behaviour depends on the monad. Due to a design wart, calling `fail` often throws an\n    exception, even for error-tracking monads such as `Either` and `ExceptT`.\n3.  For exceptions, try to catch them as close to their source as possible and to rethrow them as errors using one of\n    the two other methods. Be careful to only catch the exceptions you know about; blindly catching all exceptions and\n    propagating them up as a `SomeException` or a `Text` will not improve the quality of your error handling, it will\n    only decrease it since your callers won't know what to handle and it will be much more difficult to make sure the\n    generated error messages are valid english sentences. It is better to let unknown exceptions propagate upwards as\n    exceptions, not errors, and to handle exceptions generically at the top-level of your program. Because of\n    asynchronous exceptions, all the code you write has to be exception-safe anyway.\n\n\n## Propagating and transforming errors\n\nOnce an error is detected, the computation stops and the error gets propagated up the stack until it gets handled.\n`on-error`'s two modules provide two alternate ways of doing that:\n\n1.  `Control.Monad.Trans.OnError` is based on `transformers`, in which case `ExceptT` should be the outermost\n    transformer.\n2.  `Control.Monad.OnError` is based on `mtl`, in which case the computation should be polymorphic in `m` and have a\n    `MonadError e m` constaint.\n\nLearning `Control.Monad.Trans.OnError` first is recommended, because its type signatures are more intuitive. The type\nsignatures of the `Control.Monad.OnError` module are a bit misleading because they often ask for an `ExceptT`\ncomputation when a computation which is polymorphic in `m` would be a better choice. For example, the type of\n`Control.Monad.OnError.mapError` is\n\n    mapError :: MonadError e' m =\u003e (e -\u003e e') -\u003e ExceptT e m a -\u003e m a\n\nAnd so we might be tempted to give it an `ExceptT e m a` computation:\n\n    countHumans :: forall m. (MonadError Text m, MonadIO m) =\u003e m Int\n    countHumans = mapError (\"while counting humans: \" \u003c\u003e) body\n      where\n        body :: ExceptT Text m Int\n        body = do\n          liftIO $ putStrLn \"counting humans...\"\n          throwE \"humans are not pets\"\n\nThis typechecks, but using such a concrete monad stack doesn't fit well with the `mtl` style for which\n`Control.Monad.OnError` is designed. It would be better to use a polymorphic monad stack:\n\n    countHumans :: forall m. (MonadError Text m, MonadIO m) =\u003e m Int\n    countHumans = mapError (\"while counting humans: \" \u003c\u003e) body\n      where\n        body :: forall n. (MonadError Text n, MonadIO n) =\u003e n Int\n        body = do\n          liftIO $ putStrLn \"counting humans...\"\n          throwError \"humans are not pets\"\n\nThis typechecks as well, since `n` automatically gets specialized to `ExceptT Text m`.\n\nThe reason `mapError` specializes the `n` of its input computation in this way is that changing the type of the error\nbeing propagated is not an effectful action in any monad, it is instead a translation from an `ExceptT e` computation to\nan `ExceptT e'` computation. By specializing `n a` to `ExceptT e m a`, we can strip off the `ExceptT e` layer to obtain\nan `m (Either e a)`, at which point we can convert the `e` to an `e'` and rethrow it using `m`'s `MonadError` instance.\nThis means that `m` will itself be instantiated to a monad stack containing an `ExceptT e'` at some point, and so if the\n`n a` computation was using a concrete monad stack, it would look something like `ExceptT e (ExceptT e' IO) a`. This is\na pretty unusual and unintuitive monad stack, which is another reason to prefer writing it as a computation which is\npolymorphic in `n`.\n\n\n## Handling errors\n\nWhile you can use the `onError[Action]` functions to handle the errors in whichever way you please, here are some\nconcrete recommendations.\n\nFor a function which looks up a key in a `Map`, it makes sense to return a `Maybe` to denote the fact that the key was\nnot found. The `Nothing` case isn't necessarily an error case; perhaps the caller wants to insert a new value at that\nkey, and the `Nothing` case is actually the success case because there isn't an existing, conflicting value at that key.\n\nSo when we are very close to the source of the \"error\", it's not yet clear whether that error is problematic or not,\nbecause we do not yet have enough context. So we propagate the information upwards, in the hope that the caller has more\ncontext.\n\nIf we are manipulating a graph represented as a `Map` from node to neighbours, we know that our invariant is that all\nthe neighbour nodes must be present in the `Map`. So if we attempt to perform a lookup and we receive a `Nothing`, we\nknow that we have a bug somewhere which accidentally breaks the invariant. There is nothing the caller can do about\nthis, the only solution is to abort and to inform the programmer that a bug needs to be fixed. Since there is nothing\nthe caller can do, it is not useful to tell it that this particular error case could happen, and so for bugs, I don't\nrecommend propagating the error up using `on-error`, instead I recommend failing with an exception, for example using\n`error \"invariant violated: neighbour not in Map\"`.\n\nKeeping track of such invariants in order to know when to convert unlikely errors into exceptions which will hopefully\nnever be thrown is an important part of error-handling, because it allows you to reduce the number of error cases you\nare propagating up. Otherwise, as we go up the stack, functions have more and more sub-calls beneath them, and so more\nand more error cases would accumulate, and handling all those cases would become unmanageable. I recommend trying to\nkeep the number of error cases small at all levels.\n\nAt the top-level, the caller is the user. For them, a sum type describing all the possible error cases is less useful;\nwhat they need is a clear error message. So once we have enough context to know that an error cannot be handled by the\ncode and will have to be displayed to the user, I recommend converting the value representing the error to `Text`, and to\npropagate that error message upwards. With judicious uses of `annotateError`, this error message can be annotated with\nsome contextual information clarifying where the error has occurred.\n\nFor example, if the user provides a pair of keys so we can perform some lookup in a nested `Map` of `Map`s, we'll have\nto tell the user which key wasn't found and in which `Map`:\n\n    lookupM :: MonadError Text m\n            =\u003e Int -\u003e Map Int a -\u003e m a\n    lookupM k = onNothingThrowError (\"key \" \u003c\u003e showt k \u003c\u003e \" not found\")\n              . Map.lookup k\n\n    nestedLookupM :: MonadError Text m\n                  =\u003e (Int, Int) -\u003e Map Int (Map Int a) -\u003e m a\n    nestedLookupM (k1, k2) mm = do\n      m \u003c- annotateError \"outer map\" $ lookupM k1 mm\n      a \u003c- annotateError \"inner map\" $ lookupM k2 m\n      pure a\n\n    -- |\n    -- \u003e\u003e\u003e nestedLookupM (3,4) nestedMap :: Either Text Int\n    -- Left \"outer map: key 3 not found\"\n    -- \u003e\u003e\u003e nestedLookupM (1,4) nestedMap :: Either Text Int\n    -- Left \"inner map: key 4 not found\"\n    nestedMap :: Map Int (Map Int Int)\n    nestedMap = Map.fromList [(1, Map.fromList [(2, 42)])]\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterbecich%2Fon-error","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeterbecich%2Fon-error","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterbecich%2Fon-error/lists"}