{"id":16964968,"url":"https://github.com/propensive/contingency","last_synced_at":"2025-03-22T14:31:01.755Z","repository":{"id":186741992,"uuid":"675671217","full_name":"propensive/contingency","owner":"propensive","description":"Safe and seamless validation and error aggregation","archived":false,"fork":false,"pushed_at":"2025-01-26T12:13:40.000Z","size":1919,"stargazers_count":5,"open_issues_count":3,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-18T11:52:01.806Z","etag":null,"topics":["checked-exceptions","error-collection","exception-handling","scala","unchecked-exceptions","validation"],"latest_commit_sha":null,"homepage":"https://soundness.dev/contingency/","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/propensive.png","metadata":{"files":{"readme":".github/readme.md","changelog":null,"contributing":".github/contributing.md","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":"2023-08-07T13:05:44.000Z","updated_at":"2025-01-26T12:13:43.000Z","dependencies_parsed_at":null,"dependency_job_id":"fd7e9c11-7bf9-4277-ac48-baad017e3b73","html_url":"https://github.com/propensive/contingency","commit_stats":null,"previous_names":["propensive/perforate","propensive/contingency"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fcontingency","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fcontingency/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fcontingency/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fcontingency/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/propensive","download_url":"https://codeload.github.com/propensive/contingency/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244971777,"owners_count":20540854,"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":["checked-exceptions","error-collection","exception-handling","scala","unchecked-exceptions","validation"],"created_at":"2024-10-13T23:44:43.146Z","updated_at":"2025-03-22T14:31:01.746Z","avatar_url":"https://github.com/propensive.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"[\u003cimg alt=\"GitHub Workflow\" src=\"https://img.shields.io/github/actions/workflow/status/propensive/contingency/main.yml?style=for-the-badge\" height=\"24\"\u003e](https://github.com/propensive/contingency/actions)\n[\u003cimg src=\"https://img.shields.io/discord/633198088311537684?color=8899f7\u0026label=DISCORD\u0026style=for-the-badge\" height=\"24\"\u003e](https://discord.com/invite/MBUrkTgMnA)\n\u003cimg src=\"/doc/images/github.png\" valign=\"middle\"\u003e\n\n# Contingency\n\n__Versatile error handling for every scenario.__\n\n__Contingency__ is an experimental library for abstracting over error handling\nstrategies. In particular, it gives developers a choice between throwing\nexceptions, returning errors in a variety of datatypes, and accumulating\nseveral validation-style errors. Code must be written to accomodate\nContingency's generic error handling, but the changes from exception-throwing\ncode are trivial.\n\n## Features\n\n- error are checked as _capabilities_\n- choose global and localized strategies for error-handling\n- fully-typesafe error handling\n- selectively ignore errors considered \"impossible\"\n- aggregate multiple errors, like a _validation_\n- recover from specific errors with success values\n- mitigate specific errors into more general errors\n\n\n## Availability\n\nContingency has not yet been published. The medium-term plan is to build it with\n[Fury](https://github.com/propensive/fury) and to publish it as a source build\non [Vent](https://github.com/propensive/vent). This will enable ordinary users\nto write and build software which depends on Contingency.\n\nSubsequently, Contingency will also be made available as a binary in the Maven\nCentral repository. This will enable users of other build tools to use it.\n\nFor the overeager, curious and impatient, see [building](#building).\n\n\n\n\n\n\n## Getting Started\n\nAll Contingency terms and types are defined in the `contingency` package:\n```amok\nsyntax  scala\n##\nimport contingency.*\n```\nand are exported to `soundness`. So alternatively,\n```amok\nsyntax  scala\n##\nimport soundness.*\n```\n\n_Contingency_ provides a number of different strategies and tactics for\nhandling errors in Scala, for libraries which opt into its advanced\ncapabilities.\n\nContingency's approach builds upon the new `boundary`/`break` infrastructure in\nScala 3 to provide comprehensive errors-handling functionality which is:\n - composable: write in a direct-style, and compose expressions seamlessly\n - typesafe: error handling is statically checked\n - performant: avoid costly construction of stack traces\n - versatile: choose different tactics for different circumstances, or global\n   strategies\n\n## Examples\n\nHere is a quick tour of how error handling in Scala can be versatile,\ncomposable, typesafe and performant with Contingency.\n\nLet's start by declaring a partial method with a `raises` clause in its return\ntype, and abort under certain conditions:\n```amok\nsyntax  scala\ntransform\n  replace   Bytes  Bytes raises AsciiError\n##\ndef convert(message: Text): Bytes =\n  if message.exists(_.toInt \u003e 127) then abort(AsciiError())\n  message.bytes\n```\n\nWe cannot call that method unless its `AsciiError` is handled in some way. This\ncode will _not_ compile:\n```amok\nsyntax  scala\n##\nval data = convert(t\"Hello world\")\n```\n\nOne solution is to import a _strategy_ to handle any possible errors by\nthrowing them. This works well for code that is still at the \"prototype\" stage\nof development:\n```amok\nsyntax  scala\n##\nimport strategies.throwUnsafely\nval data = convert(t\"Hello world\")\n```\n\nBut we can get the same effect more _locally_ by wrapping the invocation in\n`unsafely`,\n```amok\nsyntax  scala\n##\nval data: Bytes = unsafely(convert(t\"Hello world\"))\n```\nor `safely`,\n```amok\nsyntax  scala\n##\nval data: Optional[Bytes] = safely(convert(t\"Hello world\"))\n```\nwhich will return the optional `Unset` value in the event of an error—the\nerror object itself will be discarded, though.\n\nIn some circumstances, we might decide that it is acceptable to _throw_ an\n`AsciiError` (without saying anything about other error types), and we can do\nso by by declaring an `Unchecked` instance for it, like so:\n```amok\nsyntax  scala\n##\nerased given AsciiError is Unchecked\n```\n\n(This is only a \"marker\" typeclass, so it can be `erased`.)\n\nWe could go further and declare `AsciiError`s as \"fatal\", shutting down the\nentire JVM upon the first occurrence,\n```amok\nsyntax  scala\n##\ngiven AsciiError is Fatal = _ =\u003e ExitStatus.Fail(1)\nval data: Bytes = convert(t\"Hello world\")\n```\nthough this might be more typical for errors that are raised during\ninitialization.\n\nNow imagine we want to combine three methods, `Json.parse`, `Json#as` and\n`convert`, with signatures,\n```amok\nsyntax  scala\n##\nobject Json:\n  def parse(text: Text): Json raises ParseError\n\nclass Json():\n  def as[ResultType]: ResultType raises AccessError\n\n// implementation details not shown\n```\nin a single method, `processEvent`. We would be required to handle\n`ParseError`s, `AccessError`s and `AsciiError`s. We could write,\n```amok\nsyntax  scala\n##\ndef processEvent(event: Text)\n        : Bytes raises ParseError raises AccessError raises AsciiError =\n  convert(Json.parse(event).as[Event].message)\n```\nbut multiple `raises` clauses are cumbersome: not only does the method need to\ndeclare each error type, any method which invokes it must also handle _all_ of\nthese errors.\n\nInstead, we can _tend_ them into an `EventError`:\n\n```amok\nsyntax  scala\n##\ndef eventData(event: Text): Bytes raises EventError =\n  tend:\n    case ParseError()  =\u003e EventError()\n    case AccessError() =\u003e EventError()\n    case AsciiError()  =\u003e EventError()\n  .within:\n    convert(Json.parse(event).as[Event].message)\n```\n\nThis has the effect that any exception matching one of the `tend` cases will\nbe transformed into the right-hand side of the case—in this case, a new\n`EventError`.\n\nThis may be more typical for production code. But note how the main expression,\n`sendAscii(Json.parse(event).as[Event].message)`, remains the same as it would\nif we had used the `throwUnsafely` strategy. This is the beauty of direct-style\nScala: the \"happy path\" can be written with the same elegance, concision and\naesthetics, even after enhancing the safety of the code.\n\nUsing alternative definitions of `ParseError`, `AccessError`, `AsciiError` and\n`EventError`s as immutable datatypes _with parameters_, we could channel details\nfrom one type to the other, like so:\n\n```amok\nsyntax  scala\n##\ndef processEvent(event: Text): Unit raises EventError =\n  tend:\n    case ParseError(line) =\u003e EventError(m\"invalid JSON at line $line\")\n    case AccessError(key) =\u003e EventError(m\"key $key was missing\")\n    case AsciiError()     =\u003e EventError(m\"the message contained invalid ASCII\")\n  .within:\n    send(convert(Json.parse(event).as[Event].message))\n```\n\nIn this example, every error on the right-hand side has the same type, and while\nthat is a common use-case, it's not a requirement. The only constraint is that\nthe type of each right-hand side case is (independent of the other cases) an\n`Exception` type that has a handler. In this example, the `raises EventError`\nin the return type ensures that handler.\n\nSometimes, however, in the event of certain errors, we want to _return_ a\nvalue—some sort of \"fallback\" value—instead of continuing along an\nerror-recovery path. For this, we can use `mend` instead of `tend`, and the\nright-hand side of each case will represent the return value:\n```amok\nsyntax  scala\n##\ndef processEvent(event: Text): Unit raises EventError = send:\n  mend:\n    case ParseError(_)  =\u003e Bytes(0, 1)\n    case AccessError(_) =\u003e Bytes(0, 2)\n    case AsciiError(_)  =\u003e Bytes(0, 3)\n  .within:\n    convert(Json.parse(event).as[Event].message)\n```\n\nHere, we _mend_ the subexpression\n`convert(Json.parse(event).as[Event].message)` and produce a two-byte message\n(such as `Bytes(0, 1)`, which could be a representation of the failure). Either\nthis, or the successful evaluation of the subexpression will be passed to the\n`send` method.\n\n## Core concepts\n\nA _partial method_ is a method which may not produce a result for certain\ninputs, i.e. it is not _total_. Partial methods are already familiar in Scala\n(and Java), but the way they handle the _absence_ of a result is to\nthrow a traditional exception, determined directly or indirectly by the code in\nthe method's implementation.\n\nNote that Functional Programming requires all functions to be _total_. (If they\nare not, then they're not even considered functions.) Partiality can be\nencoded in this strict definition of a function in a variety of ways, but\ninvariably they return, in their _total_ encoding, values representing the\nabsence of a return value in their unencoded _partial_ form.\n\nWithout exceptions, if it's not possible to return a successful value from a\nmethod, we need to \"abort\" execution somehow, or return a \"non-value\". Here's a\ntrivial example of a method where that's necessary,\n```amok\nsyntax  scala\n##\ndef second[ElementType](list: List[ElementType]): ElementType =\n  if list.length \u003e= 2 then list(1) else ???\n```\nwhere `???` indicates the code we are unable to implement.\n\nContingency provides the `abort` method for indicating a failure, which can be\nthought of as similar to the `throw` keyword: it \"escapes\" from the current\nmethod with an error, instead of returning a value.\n\nIn the implementation above, we can replace `???` with `abort(TooShort(2))`,\nassuming a `TooShort` exception type such as:\n```amok\nsyntax  scala\n##\ncase class TooShort(minimum: Int)\nextends Exception(s\"A minimum length of $minimum is required\")\n```\n\nNote that the error type must be a subtype of `Exception` because some\nstrategies may need to throw it.\n\nHowever, we can _only_ call `abort` with an error if we have a `Tactic` in\nscope for its error type. In this case, we _must_ have a contextual\n`Tactic[TooShort]` available.\n\nOne way to provide it is to change the method signature to require it, like so,\n```amok\nsyntax  scala\n##\ndef second[ElementType](list: List[ElementType])\n    (using Tactic[TooShort])\n        : ElementType =\n  if list.length \u003e= 2 then list(1)\n  else abort(TooShortError(2))\n```\nwhich may be more easily expressed using the infix `raises` type:\n```amok\nsyntax  scala\n##\ndef second[ElementType](list: List[ElementType])\n        : ElementType raises TooShortError =\n  if list.length \u003e= 2 then list(1)\n  else abort(TooShortError(2))\n```\n\nThe infix `raises` type is just a syntactic alias. The type\n`ElementType raises TooShortError` is equivalent to the context function type,\n`Tactic[TooShortError] ?=\u003e ElementType`, which is equivalent to specifying the\n`using Tactic[TooShortError]` parameter.\n\nIn practice, though, we can usually just append `raises ErrorType` to the\nreturn type. This just defers the problem, though; calling any method declared,\n`raises ErrorType`, needs an `ErrorType` in-scope at the callsite. We can\ncontinue adding more `raises ErrorType` declarations, but at some point it is\nnecessary to _handle_ the error, which requires an instance of\n`Tactic[ErrorType]`.\n\nIt is the `Tactic` instance which determines exactly how the error is handled:\nwhether it is thrown, logged, aggregated, or something else. And by passing it\nin as a parameter to the method, we are delegating the handling choice back up\nto the methods that called it.\n\nIn other words, we have just implemented the method for all error-handling\nstrategies.\n\n### Non-terminal Errors\n\nIn all cases, `abort` will stop execution and pass control up the stack to the\npoint where the error is handled. (Safely-checked exceptions ensure that there\nmust be such a place.) But sometimes, we want to _accommodate_ the possibility\nthat, even though failure is inevitable, execution may continue for a while,\nwith one purpose in mind: to accrue additional errors.\n\nA typical use-case is validating a form containing several fields. Any one of\nthe values provided for the field may yield an error, but if the form contains\nseveral errors, we would like to see all of them _together_; not just the first.\n\nThis becomes possible with certain implementations of `Tactic`, but it requires\ncooperation from the implementation. That is provided through an\nalternative to `abort`, called `raise`.\n\nWhen we call `abort`, it allows us to \"exit\" a method without returning a value.\nIt is the absence of any value to return which requires this, and this is\nreflected in `abort`'s return type: `Nothing`. But `raise` _does_ return a\nvalue—an _ersatz_ or _substitute_ value—which can let execution continue\n(using that value) locally, while registering the error and asserting that\nan error will be produced, a little later.\n\nAs execution continues, additional `raise` invocations may be encountered,\ncorresponding to more errors. Each of these will be registered by the `Tactic`,\nand execution may complete all the way to return a final result for at the point\nwhere the errors are handled. But having registered at least one error during\nexecution, that final result will be considered invalid and is discarded. And\ninstead, a new error corresponding to the aggregation of each recorded error\nwill be produced.\n\nSo, by proceeding with execution within the bounds of error checking, it becomes\npossible to accrue several \"trivial\" errors before they manifest into an error\nthat requires handling.\n\nBut this only works under certain conditions. When we provide an ersatz value\nin place of an error, that value must be _inconsequential_. That means there\nshould not be additional code which depends upon its value. We are somewhat\nprotected from consequentiality by the knowledge that a single `raise`d error\ncannot affect the end result, because that value is guaranteed to be discarded.\nBut this guarantee does not apply to side-effects, including additional errors\nwhich may be raised as a consequence of the ersatz value. Ideally, ersatz values should be innocuous and independent.\n\n### Performance\n\nOne advantage of using Scala's `boundary` and `break` infrastructure instead of\n_throwing_ exceptions is that the costly construction of a stack trace can be\navoided (optionally), and construction of an error is no more expensive than\nany other immutable datatype.\n\n### Strategies\n\nEach error must be handled by a `Tactic`, which will typically be constrained\nto a limited scope—often the `within` block of a `mend` or `tend`, or a\n`safely` or `unsafely` block. They are _tactics_ in the sense that they apply\nwithin a limited scope.\n\nTactics contrast with a _strategy_, whose implied scope is wider or global. In\nterms of implementation, they are no different: instances of `Tactic`. But we\ncan informally call them _strategies_ when used globally. For example, the\n`throwUnsafely` strategy which provides a universal `Tactic` instance that just\nthrows any error that's raised.\n\nOther strategies might be `Fatal` instances defined in package-level scope, for\nexample,\n```amok\nsyntax  scala\n##\npackage app\n\ngiven InitError is Fatal = error =\u003e\n  Log.info(m\"Error during initialization: $error\")\n  ExitStatus(127)\n```\nwhich specifies that any `InitError` should cause the JVM to exit, after\nlogging the error.\n\n\n## Status\n\nContingency is classified as __embryotic__. For reference, Soundness projects are\ncategorized into one of the following five stability levels:\n\n- _embryonic_: for experimental or demonstrative purposes only, without any guarantees of longevity\n- _fledgling_: of proven utility, seeking contributions, but liable to significant redesigns\n- _maturescent_: major design decisions broady settled, seeking probatory adoption and refinement\n- _dependable_: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version `1.0.0` or later\n- _adamantine_: proven, reliable and production-ready, with no further breaking changes ever anticipated\n\nProjects at any stability level, even _embryonic_ projects, can still be used,\nas long as caution is taken to avoid a mismatch between the project's stability\nlevel and the required stability and maintainability of your own project.\n\nContingency is designed to be _small_. Its entire source code currently consists\nof 647 lines of code.\n\n## Building\n\nContingency will ultimately be built by Fury, when it is published. In the\nmeantime, two possibilities are offered, however they are acknowledged to be\nfragile, inadequately tested, and unsuitable for anything more than\nexperimentation. They are provided only for the necessity of providing _some_\nanswer to the question, \"how can I try Contingency?\".\n\n1. *Copy the sources into your own project*\n   \n   Read the `fury` file in the repository root to understand Contingency's build\n   structure, dependencies and source location; the file format should be short\n   and quite intuitive. Copy the sources into a source directory in your own\n   project, then repeat (recursively) for each of the dependencies.\n\n   The sources are compiled against the latest nightly release of Scala 3.\n   There should be no problem to compile the project together with all of its\n   dependencies in a single compilation.\n\n2. *Build with [Wrath](https://github.com/propensive/wrath/)*\n\n   Wrath is a bootstrapping script for building Contingency and other projects in\n   the absence of a fully-featured build tool. It is designed to read the `fury`\n   file in the project directory, and produce a collection of JAR files which can\n   be added to a classpath, by compiling the project and all of its dependencies,\n   including the Scala compiler itself.\n   \n   Download the latest version of\n   [`wrath`](https://github.com/propensive/wrath/releases/latest), make it\n   executable, and add it to your path, for example by copying it to\n   `/usr/local/bin/`.\n\n   Clone this repository inside an empty directory, so that the build can\n   safely make clones of repositories it depends on as _peers_ of `contingency`.\n   Run `wrath -F` in the repository root. This will download and compile the\n   latest version of Scala, as well as all of Contingency's dependencies.\n\n   If the build was successful, the compiled JAR files can be found in the\n   `.wrath/dist` directory.\n\n## Contributing\n\nContributors to Contingency are welcome and encouraged. New contributors may like\nto look for issues marked\n[beginner](https://github.com/propensive/contingency/labels/beginner).\n\nWe suggest that all contributors read the [Contributing\nGuide](/contributing.md) to make the process of contributing to Contingency\neasier.\n\nPlease __do not__ contact project maintainers privately with questions unless\nthere is a good reason to keep them private. While it can be tempting to\nrepsond to such questions, private answers cannot be shared with a wider\naudience, and it can result in duplication of effort.\n\n## Author\n\nContingency was designed and developed by Jon Pretty, and commercial support and\ntraining on all aspects of Scala 3 is available from [Propensive\nO\u0026Uuml;](https://propensive.com/).\n\n\n\n## Name\n\n_Contingency_ (the library) provides various forms of mitagation and\ncontingency in the event that an exception occurs at runtime.\n\nIn general, Soundness project names are always chosen with some rationale,\nhowever it is usually frivolous. Each name is chosen for more for its\n_uniqueness_ and _intrigue_ than its concision or catchiness, and there is no\nbias towards names with positive or \"nice\" meanings—since many of the libraries\nperform some quite unpleasant tasks.\n\nNames should be English words, though many are obscure or archaic, and it\nshould be noted how willingly English adopts foreign words. Names are generally\nof Greek or Latin origin, and have often arrived in English via a romance\nlanguage.\n\n## Logo\n\nThe logo shows three tickets, each of which has been _validated_.\n\n## License\n\nContingency is copyright \u0026copy; 2025 Jon Pretty \u0026 Propensive O\u0026Uuml;, and\nis made available under the [Apache 2.0 License](/license.md).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpropensive%2Fcontingency","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpropensive%2Fcontingency","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpropensive%2Fcontingency/lists"}