{"id":15457032,"url":"https://github.com/expede/rescue","last_synced_at":"2025-06-16T21:38:24.269Z","repository":{"id":45086631,"uuid":"249357011","full_name":"expede/rescue","owner":"expede","description":"🚒✨ Rescue: better errors through types (a more type directed MonadThrow/MonadCatch)","archived":false,"fork":false,"pushed_at":"2022-02-01T07:42:47.000Z","size":264,"stargazers_count":20,"open_issues_count":5,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-22T10:22:32.266Z","etag":null,"topics":["error-handling","library"],"latest_commit_sha":null,"homepage":"https://hackage.haskell.org/package/rescue","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/expede.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-03-23T06:54:39.000Z","updated_at":"2024-01-22T17:28:31.000Z","dependencies_parsed_at":"2022-09-04T17:10:37.947Z","dependency_job_id":null,"html_url":"https://github.com/expede/rescue","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/expede/rescue","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Frescue","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Frescue/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Frescue/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Frescue/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/expede","download_url":"https://codeload.github.com/expede/rescue/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Frescue/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260246197,"owners_count":22980353,"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":["error-handling","library"],"created_at":"2024-10-01T22:41:37.990Z","updated_at":"2025-06-16T21:38:24.246Z","avatar_url":"https://github.com/expede.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🚒✨ Rescue\n## More Understandable Error Handling\n\n![Continuous Integration](https://github.com/fission-suite/fission/workflows/Continuous%20Integration/badge.svg)\n![License](https://img.shields.io/github/license/expede/rescue)\n\n`rescue` is an experimental error handling library for Haskell.\n\nThe standard library approach to error handling is to use an existential type to\n_subclass_ any error as `SomeException`. This is very convenient \n(requires nealy zero set up), and matches what  developers expect coming from \nother ecosystems.\n\nA core goal of `rescue` is to give the programmer clarity about which execptions\nare possible at any given point in the code. We achieve this by using (open) variants\nfor compositon rather than inheritance. We hide the detail as much as posisble,\nand attempt to make this work in a flexible, constraint-driven style as much \nas possible.\n\nPerhaps there is some value in treating these more separately. \nThere is an old distinction that mirrors the above as errors (irrecoverable) \nvs exceptions (recoverable).\n\n`rescue` breaks the problem into synchronous and asynchoronous exceptions.\nThis distinction allows us to express our assumptions about the error \nenvironment, separate modes of handling for the same monadic stack, \nand even different errors.\n\n# Goals\n\nOur goals are to have:\n\n* Clarity of what errors can be raised by some code\n* Not need a large tree of error types for our application (set-like)\n* Way to declare assumptions about which errrors are available\n  * e.g. \"can raise X, Y, and Z\", or \"raises exactly X and Y but none others\"\n* Extensible\n* Ability to call code that raises some subset of errors\n* Handle (and eliminate) exceptions from the context\n\n# Approach\n\nThe closest approach is `MonadError`, except that our implementation uses \ntype-level lists for flexiblity,  and hides the exception parameter from the \nclass constraint as a type family.  `rescue` also splits `raise` and `attempt` \ninto separate classes to help us more granularly express the effects available \nin the current context. \n\n## Class Heirarchy\n\n1. `MonadRaise` -- roughly `MonadThrow`\n2. `MonadRescue` -- roughly `MonadCatch`\n3. `MonadCleanup` -- roughly `MonadBracket`\n\n`Monad m =\u003e MonadRaise m =\u003e MonadRescue m =\u003e MonadCleanup m`\n\n## Errors / Asynchronous Exceptions\n\nAn asynchronous error (e.g. coming from another thread)\ncomes with a lot more uncertainty. We may not be aware of the type of error\nbeing thrown to us, or when it's thrown (since the runtime will interrupt).\nThe purpose of this scenraio is typically to cleanup some resource \nand immeditely rethrow.\n\nWhile a more structured, type-driven style would be appreciated here, this is\nfundamentally how the runtime works. As such, we do need to contend \nwith `SomeAsyncException`s.\n\nThe good news is that we're not expected to do much with them. Essentially all\nwe care about is:\n\n* _That_ an async exception interrupted our flow, \n* We should not attempt recovery\n* The error should be rethrown\n\nCleanup is also a use case for synchronous exceptions. If something is thrown\ninside a context with an open resource, we should clean that up.\n\nThe basic flow for an async execption is then:\n\n1. An async exception is raised\n2. Convert it to a synchronous exception \n3. Run the normal cleanup\n4. Rethrow the original asynchronous error\n\nThis means that any instacne of MonadAsyncCleanup needs a MonadCleanup and `Raises m SomeAsyncException`\n\n# FAQ\n\n## Why another typeclass?\n\nIt's true that MonadThrow is totally pervasive. However, not trying to \n\nThere's also nothing stopping us from writing a function of the type\n\n```haskell\nfromThrow :: (MonadCatch m, MonadRaise n) =\u003e m a -\u003e n a\n```\n\n...though the conversion from `SomeException` would take a bit of care.\n\n## Why avoid `SomeException`?\n\n`SomeException` is typesafe, but very difficult to track by hand.\nIn fact, with async exceptions, you may need to handle an error interrupting\nyour execution _even if you don't have a `MonadThrow`_ in your context.\n\nBy treating exceptions as something visible and tarckable (though hidden\nwhen you don't need it), we gain a lot of ability to reason about our program,\nand avoid writing lots of nested `Either`s.\n\n## Does this do async exception handling?\n\nIt does! `MonadCleanup` is the typeclass, and it has very few instances.\nIt's essentially [`MonadBracket`](https://www.fpcomplete.com/blog/2017/02/monadmask-vs-monadbracket/),\nbut with explicit errors.\n\nThis separation of the `SomeException` and `OpenUnion` can help determine the\nintention of the exception. `SomeException` (and `SomeAsyncException`) are really\nmore like errors -- things that should fail and not be recovered from. In a world\nwith async exceptions, the cleanest way to respond to an error is to stop\nour execution, cleanup any resources, and propogate the error.\n\n## Are there other packages attempting to solve this problem?\n\nYep! For instance, [`safe-exceptions`](https://hackage.haskell.org/package/safe-exceptions).\nThey distinguish between three kinds of exceptions:\n\n\u003e We're going to define three different versions of exceptions. Note that these definitions are based on how the exception is thrown, not based on what the exception itself is:\n\u003e\n\u003e    **Synchronous** exceptions are generated by the current thread. What's important about these is that we generally want to be able to recover from them. For example, if you try to read from a file, and the file doesn't exist, you may wish to use some default value instead of having your program exit, or perhaps prompt the user for a different file location.\n\u003e\n\u003e    **Asynchronous** exceptions are thrown by either a different user thread, or by the runtime system itself. For example, in the async package, race will kill the longer-running thread with an asynchronous exception. Similarly, the timeout function will kill an action which has run for too long. And the runtime system will kill threads which appear to be deadlocked on MVars or STM actions.\n\u003e\n\u003e    In contrast to synchronous exceptions, we almost never want to recover from asynchronous exceptions. In fact, this is a common mistake in Haskell code, and from what I've seen has been the largest source of confusion and concern amongst users when it comes to Haskell's runtime exception system.\n\n\u003e    **Impure** exceptions are hidden inside a pure value, and exposed by forcing evaluation of that value. Examples are error, undefined, and impureThrow. Additionally, incomplete pattern matches can generate impure exceptions. Ultimately, when these pure values are forced and the exception is exposed, it is thrown as a synchronous exception.\n\u003e\n\u003e    Since they are ultimately thrown as synchronous exceptions, when it comes to handling them, we want to treat them in all ways like synchronous exceptions. Based on the comments above, that means we want to be able to recover from impure exceptions.\n\n...along with some general guidance...\n\n\u003e   All synchronous exceptions should be recoverable\n\u003e   All asynchronous exceptions should not be recoverable\n\u003e   In both cases, cleanup code needs to work reliably\n\nRescue is primarily focused on deligering a great experince when dealing with \nsynchronous excpetions, though touches on async ones as well. \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexpede%2Frescue","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexpede%2Frescue","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexpede%2Frescue/lists"}