A monad transformer that allows you to throw and catch a restricted set of exceptions, tracked at the type level.
Requires GHC 9.8+ for the core library. The type-checker plugin (checked-exceptions:plugin) requires GHC 9.10+ with a matching ghc package.
Add to your .cabal file:
build-depends:
checked-exceptions
, checked-exceptions:plugin
ghc-options: -fplugin Control.Monad.CheckedExcept.PluginThen in Haskell (the OPTIONS_GHC pragma is optional if you set ghc-options in Cabal):
{-# OPTIONS_GHC -fplugin Control.Monad.CheckedExcept.Plugin #-}
{-# LANGUAGE
TypeApplications
, DataKinds
, StandaloneDeriving
, DerivingVia
, QualifiedDo
, FlexibleInstances
#-}
type TestExceptions = '[(), Int, Bool, String]
testCE :: CheckedExceptT TestExceptions IO ()
testCE = CheckedExcept.do
() <- testCE1 :: CheckedExceptT '[()] IO ()
() <- testCE2 :: CheckedExceptT '[Int] IO ()
() <- testCE3 :: CheckedExceptT '[Bool] IO ()
() <- testCE4 :: CheckedExceptT '[String] IO ()
-- () <- testCE5 :: CheckedExceptT '[Char] IO () -- doesn't compile
pure ()
test :: CheckedExcept TestExceptions () -> IO ()
test ce = case runCheckedExcept ce of
Left e -> do
applyAll (putStrLn . encodeException) e
withOneOf @() e $ \() -> putStrLn "()"
withOneOf @Int e $ \n -> print $ n + 1
withOneOf @Bool e $ \_ -> pure ()
caseException e
( (\() -> putStrLn "()")
<: (\n -> print $ n + 1)
<: CaseAny (\x -> putStrLn $ encodeException x)
-- <: (\b -> putStrLn "bool")
-- <: (\s -> putStrLn "string")
-- <: CaseEnd
)
Right () -> putStrLn "Right"The facilities this library provides will alert you when you have, intentionally or unintentionally, introduced a new possible exception in your code that is presently unaccounted for. Since we enforce at the type level what kinds of exceptions are permissible, you can safely trust the exceptions set in the type signature to do something like generate OpenAPI documentation for an HTTP handler's error responses.
When catching an exception, we provide the CaseException type to allow coverage checking with a case-like API (caseException), or you can use methods provided by the CheckedException typeclass to perform common operations on exceptions without inspecting the type of the exception.
Elem and Contains are type classes backed by value-level witnesses:
ElemIx e es— index ofeinsidees(Here/There)Subset es1 es2— every element ofes1appears ines2(SubRefl,SubNil,SubCons)lookupSubset— translate anElemIxalong aSubsetwitnesscontainsRefl— reflexiveSubset es esfor abstract exception lists
OneOf is constructed with oneOf, not a data constructor pattern. The internal constructor carries an ElemIx witness so subset widening (weakenOneOf, weakenExceptions) is structurally total.
Breaking change (0.3): Contains es es is not auto-derived for abstract es. Pass an explicit witness:
weakenExceptionsWith containsRefl (action :: CheckedExceptT es m a)
:: CheckedExceptT es m aUse duplicate-free exception lists (or Nub at the kind level): duplicate types pick the first ElemIx index.
Required for QualifiedDo blocks — see the Example for Cabal setup (checked-exceptions:plugin in build-depends plus -fplugin).
The plugin lives in a separate public sublibrary so the core library does not depend on ghc. It must link against the compiler's ghc package (not ghc-lib): static -fplugin loading requires plugin :: ghc:GHC.Plugins.Plugin.
The plugin proposes default values for ambiguous exception-list metavariables created by >>= in QualifiedDo blocks (and similar). It walks stuck Elem e alpha and Contains es alpha constraints (including implication givens in nested contexts) where alpha is an unfilled [Type] metavariable, and proposes:
alpha := '[]when there are no lower bounds (coverslift/pure/returnin a do block)alpha := Nub (union of lower bounds)alpha := ubfor each concrete upper bound
GHC verifies each proposal; only a solving assignment is committed. No fiat coercions are emitted. The old plugin rewrite/solve path is removed.
Optional tracing: -fplugin-opt Control.Monad.CheckedExcept.Plugin:verbose
QualifiedDo >>= unions exception sets with Nub (es1 ++ es2) in the result type so binds accumulate exceptions without ambiguous metavariables when possible.
DerivingVia with ShowException or ExceptionException is supported. fromOneOf unwraps the newtype correctly when reading bare values from OneOf.
Custom CheckedException instances should only return Just from fromOneOf when the payload type matches e (same contract as the default eqT witness path). withOneOf uses that witness path directly and does not depend on a custom fromOneOf.