Exception Handling
Table of Contents
1 Exception Handling
1.1 Overview
Exceptions are events that happens during run-time that disrupts the normal flow of execution. The common sources of exceptions are partial functions (functions not defined to all its domain), user-thrown exceptions and IO.
The Haskell static typing system by itself frees the developer from "NullPointer Exceptions" and "value error" exceptions that happens when the code tries to sum a string and int in untyped languages. The compiler notifies the developer that it happened not allowing the code to run or go on production before it is solved.
- The type system avoids value error exceptions and null pointer exceptions avoiding unpleasant surprise at run-time.
- Maybe or Option type avoids null checking and Null pointer exceptions and raising exceptions for invalid data.
- Either type is similar to Maybe, however it allows to report the failure or what is wrong with input.
- Summarizing: The strong static typing system will help to avoid many hours of debugging trying to find where the null pointer exception or value error exception comes from, customer complaints and headaches.
See also:
- Smarter validation
- Railway oriented programming | F# for fun and profit
- Replacing Throwing Exceptions with Notification in Validations
- language agnostic - Maybe monad vs exceptions - Software Engineering Stack Exchange
- Some Thoughts on Error Handling - @codemonkeyism
- https://blogs.janestreet.com/making-something-out-of-nothing-or-why-none-is-better-than-nan-and-null/][Making something out of nothing (or, why None is better than NaN and NULL)">Jane Street Tech Blogs]]
- A Monad in Practicality: First-Class Failures - Quils in Space
- Data.Validation
- Data.Either.Validation
1.2 Avoiding Exceptions in Pure functions
1.2.1 Overview
For pure functions and functions that performs validation like
valideDateOfBirth the best way is to indicate that the function may
fail making it explicit on the type system using Maybe or Either
returning Nothing
for Maybe type or Left <error-type>
for Either
type.
1.2.2 Avoiding Exception in partial functions
Examples of partial functions:
> head [10, 20, 30] 10 it :: Num a => a > > head [] *** Exception: Prelude.head: empty list > > div 100 0 *** Exception: divide by zero > > read "100" :: Int 100 it :: Int > read "-100" :: Int -100 it :: Int > read "noise" :: Int *** Exception: Prelude.read: no parse >
Example: Avoiding exceptions using Maybe (Option type) or Either:
:{ headSafe :: [a] -> Maybe a headSafe [] = Nothing headSafe (x:_) = Just x :} > headSafe [1, 2, 3, 4, 5] Just 1 it :: Num a => Maybe a > headSafe [] Nothing it :: Maybe a > :t headSafe headSafe :: [a] -> Maybe a > :t div > div :: Integral a => a -> a -> a > :{ divSafe :: Integral a => a -> a -> Maybe a divSafe x 0 = Nothing divSafe x y = Just (div x y) :} > divSafe 100 20 Just 5 it :: Integral a => Maybe a > divSafe 100 0 Nothing it :: Integral a => Maybe a > -- Use readMaybe instead of read -- > import Text.Read (readMaybe) > > readMaybe "Just 200" :: Maybe (Maybe Int) Just (Just 200) it :: Maybe (Maybe Int) > readMaybe "Nothing" :: Maybe (Maybe Int) Just Nothing it :: Maybe (Maybe Int) > readMaybe "Notasdasdas" :: Maybe (Maybe Int) Nothing it :: Maybe (Maybe Int) > > readMaybe "[1, 2, 3, 4, 5]" :: Maybe [Int] Just [1,2,3,4,5] it :: Maybe [Int] > readMaybe "garbage" :: Maybe [Int] Nothing it :: Maybe [Int] > > readMaybe "[100.0, 200.0, 300.0]" :: Maybe [Double] Just [100.0,200.0,300.0] it :: Maybe [Double] > readMaybe ", 200.0, 300.0]" :: Maybe [Double] Nothing it :: Maybe [Double] >
1.2.3 Avoid exception in data validation
Example functions that performs data validation or user validation:
For function that performs data validation it is better to return the result as an Maybe or Either type instead of raise an Exception. Maybe is better for functions that only have one possible failure or is not necessary to know the type of failure. The Either type is useful to report error, failure information or invalid data.
import Text.Read (readMabye) :{ validateAge :: String -> Int validateAge input = case readMaybe input of Nothing -> error "Invalid age. Not a number" Just age -> case age of _ | age < 0 -> error "Error: Invalid age. It must be greater than zero." _ | age <= 18 -> error "Error: Below legal age to sign the contract." _ | age > 200 -> error "Error: Invalid age. Impossible age." _ -> age :} > validateAge "asdfa" *** Exception: Invalid age. Not a number CallStack (from HasCallStack): error, called at <interactive>:1083:17 in interactive:Ghci54 > validateAge "-100" *** Exception: Error: Invalid age. It must be greater than zero. CallStack (from HasCallStack): error, called at <interactive>:1085:35 in interactive:Ghci54 > validateAge "16" *** Exception: Error: Below legal age to sign the contract. CallStack (from HasCallStack): error, called at <interactive>:1086:35 in interactive:Ghci54 > > validateAge "300" *** Exception: Error: Invalid age. Impossible age. CallStack (from HasCallStack): error, called at <interactive>:1087:35 in interactive:Ghci54 > > validateAge "30" 30 it :: Int > validateAge "20" 20 it :: Int > validateAge "35" 35 it :: Int >
This function can be refactored to:
import Text.Read (readMabye) :{ validateAge :: String -> Either String Int validateAge input = case readMaybe input of Nothing -> Left "Invalid input. Not a number" Just age -> case age of _ | age < 0 -> Left "Error: Invalid age. It must be greater than zero." _ | age <= 18 -> Left "Error: Below legal age to sign the contract." _ | age > 200 -> Left "Error: Invalid age. Impossible age." _ -> Right age :} > validateAge "-100" Left "Error: Invalid age. It must be greater than zero." it :: Either String Int > validateAge "16" Left "Error: Below legal age to sign the contract." it :: Either String Int > validateAge "300" Left "Error: Invalid age. Impossible age." it :: Either String Int > validateAge "400" Left "Error: Invalid age. Impossible age." it :: Either String Int > validateAge "20" Right 20 it :: Either String Int > validateAge "19" Right 19 it :: Either String Int > > mapM_ print $ map validateAge ["safsdf", "-100", "garbage", "400", "7", "15", "20", "25"] Left "Invalid input. Not a number" Left "Error: Invalid age. It must be greater than zero." Left "Invalid input. Not a number" Left "Error: Invalid age. Impossible age." Left "Error: Below legal age to sign the contract." Left "Error: Below legal age to sign the contract." Right 20 Right 25 it :: () >
Algebraic data types are more friendly to pattern matching than strings. So the code above could be refactored to:
import Text.Read (readMabye) :{ data AgeError = AgeInvalidInput | AgeBelowLegalAge | AgeImpossible deriving (Eq, Show, Read) :} :{ validateAge :: String -> Either AgeError Int validateAge input = case readMaybe input of Nothing -> Left AgeInvalidInput Just age -> case age of _ | age < 0 -> Left AgeImpossible _ | age <= 18 -> Left AgeBelowLegalAge _ | age > 200 -> Left AgeImpossible _ -> Right age :} > map (\input -> (input, validateAge input)) ["safsdf", "-100", "garbage", "400", "7", "15", "20", "25"] [("safsdf",Left AgeInvalidInput),("-100",Left AgeImpossible),("garbage",Left AgeInvalidInput), ("400",Left AgeImpossible),("7",Left AgeBelowLegalAge), ("15",Left AgeBelowLegalAge),("20",Right 20), ("25",Right 25)] it :: [([Char], Either AgeError Int)] > > > mapM_ print $ map validateAge ["safsdf", "-100", "garbage", "400", "7", "15", "20", "25"] Left AgeInvalidInput Left AgeImpossible Left AgeInvalidInput Left AgeImpossible Left AgeBelowLegalAge Left AgeBelowLegalAge Right 20 Right 25 it :: () > :{ showAgeError :: AgeError -> String showAgeError age = case age of AgeBelowLegalAge -> "Error: Below legal age to sign the contract." AgeImpossible -> "Error: Invalid age. Impossible age." AgeInvalidInput -> "Invalid input. Not a number" :} > showAgeError AgeImpossible "Error: Invalid age. Impossible age." it :: String > showAgeError AgeInvalidInput "Invalid input. Not a number" it :: String > showAgeError AgeImpossible "Error: Invalid age. Impossible age." it :: String > :{ mapLeft :: (e -> b) -> Either e a -> Either b a mapLeft fn value = case value of Right a -> Right a Left e -> Left (fn e) :} > mapLeft showAgeError $ validateAge "20000" Left "Error: Invalid age. Impossible age." it :: Either String Int > mapLeft showAgeError $ validateAge "-100" Left "Error: Invalid age. Impossible age." it :: Either String Int > mapLeft showAgeError $ validateAge "10" Left "Error: Below legal age to sign the contract." it :: Either String Int > mapLeft showAgeError $ validateAge "15" Left "Error: Below legal age to sign the contract." it :: Either String Int > mapLeft showAgeError $ validateAge "25" Right 25 it :: Either String Int > mapLeft showAgeError $ validateAge "36" Right 36 it :: Either String Int >
Refactoring the function to validate multiple data:
import Text.Read (readMabye) :{ data AgeError = AgeInvalidInput | AgeBelowLegalAge | AgeImpossible deriving (Eq, Show, Read) :} type UserData = (String, Int) :{ data UserDataError = UserAgeError AgeError | UserNameError deriving(Eq, Show, Read) :} :{ mapLeft :: (e -> b) -> Either e a -> Either b a mapLeft fn value = case value of Right a -> Right a Left e -> Left (fn e) :} :{ validateAge :: String -> Either UserDataError Int validateAge input = mapLeft UserAgeError $ validateAgeAux input where validateAgeAux input = case readMaybe input of Nothing -> Left AgeInvalidInput Just age -> case age of _ | age < 0 -> Left AgeImpossible _ | age <= 18 -> Left AgeBelowLegalAge _ | age > 200 -> Left AgeImpossible _ -> Right age :} :{ validateName :: String -> Either UserDataError String validateName name = case name of "" -> Left $ UserNameError _ -> Right name :} :{ validateUser :: String -> String -> Either UserDataError UserData validateUser name age = do userName <- validateName name userAge <- validateAge age return (userName, userAge) :} -- Testing: -------------------- > validateAge "-200" Left (UserAgeError AgeImpossible) it :: Either UserDataError Int > validateAge "1000" Left (UserAgeError AgeImpossible) it :: Either UserDataError Int > validateAge "30" Right 30 it :: Either UserDataError Int > validateAge "25" Right 25 it :: Either UserDataError Int > validateAge "16" Left (UserAgeError AgeBelowLegalAge) it :: Either UserDataError Int > validateAge "25" Right 25 it :: Either UserDataError Int > > validateName "" Left UserNameError it :: Either UserDataError String > > validateName "John" Right "John" it :: Either UserDataError String > > validateUser "John" "300" Left (UserAgeError AgeImpossible) it :: Either UserDataError UserData > validateUser "John" "15" Left (UserAgeError AgeBelowLegalAge) it :: Either UserDataError UserData > validateUser "John" "-100" Left (UserAgeError AgeImpossible) it :: Either UserDataError UserData > validateUser "" "-100" Left UserNameError it :: Either UserDataError UserData > > validateUser "John" "20" Right ("John",20) it :: Either UserDataError UserData >
1.3 Catching Exceptions
1.3.1 Overview
The haskell modules Control.Exception and System.IO.Error provides functions and type constructors to deal with exceptions.
Exceptions can only be caught inside IO Monad.
Function | Signature | Summary / Short Description. | |
---|---|---|---|
Control.Exception | |||
throw | :: | Exception e => e -> a | Throw an exception. |
throwIO | :: | Exception e => e -> IO a | A variant of throw that can only be used within the IO monad. |
catch | :: | Exception e => IO a -> (e -> IO a) -> IO a | Catch exceptions. |
try | :: | Exception e => IO a -> IO (Either e a) | Similar to catch, but returns an Either. |
handle | :: | Exception e => (e -> IO a) -> IO a -> IO a | A version of catch with the arguments swapped around. |
finally | |||
evaluate | |||
System.IO.Error | |||
ioError | :: | IOError -> IO a | Raise an IOError in the IO monad. |
catchIOError | :: | IO a -> (IOError -> IO a) -> IO a | Like the fuction catch, however catchs only IO exceptions. |
tryIOError | :: | IO a -> IO (Either IOError a) | Like the function try, however only aplicable to IO exceptions. |
userError | :: | String -> IOError | Construct an IOError value with a string describing the error. |
isAlreadyExistsError | :: | IOError -> Bool | |
isDoesNotExistError | :: | IOError -> Bool | |
isAlreadyInUseError | :: | IOError -> Bool | |
isFullError | :: | IOError -> Bool | |
isEOFError | :: | IOError -> Bool | |
isIllegalOperation | :: | IOError -> Bool | |
isPermissionError | :: | IOError -> Bool | |
isUserError | :: | IOError -> Bool |
System.IO.Error predicates
Notes:
- There are no predicate functions for all IO exception types.
- The exception type constructor are not exposed, so the only way to get the type of io exception is by using the predicates functions available at module System.IO.Error.
See module: System.IO.Error for more details.
Exception type | Predicate | Exception message | Cause: |
---|---|---|---|
(abstract) | or exception string | ||
AlreadyExists | isAlreadyExistsError | already exists | File or directory already exists. |
NoSuchThing | isDoesNotExistError | does not exist | File, directory or enviroment variable |
doesn't exists. | |||
ResourceBusy | ? | resource busy | |
ResourceExhausted | ? | resource exhausted | Insufficient resources are available |
to perform the operation. | |||
EOF | isEOFError | end of file | Reached end of file while trying to read |
some line or character. | |||
IllegalOperation | isIllegalOperation | illegal operation | The implementation does not support system calls. |
PermissionDenied | isPermissionError | permission denied | The process has insufficient privileges to |
perform the operation. | |||
UserError | isUserError | user error | Exception thrown by user. |
HardwareFault | ? | hardware fault | |
InappropriateType | ? | inappropriate type | |
Interrupted | ? | interrupted | |
InvalidArgument | ? | invalid argument | |
OtherError | ? | failed | |
ProtocolError | ? | protocol error | |
ResourceVanished | ? | resource vanished | |
SystemError | ? | system error | |
TimeExpired | ? | timeout | |
UnsatisfiedConstraints | ? | unsatisfied constraints – ultra-precise! | |
UnsupportedOperation | ? | unsupported operation |
Arithmetic Exceptions: (Module: Control.Exception)
Type Constructor | Exception Message |
---|---|
Overflow | arithmetic overflow |
Underflow | arithmetic underflow |
LossOfPrecision | loss of precision |
DivideByZero | divide by zero |
Denormal | denormal |
RatioZeroDenominator | Ratio has zero denominator |
Exception Hierarchy:
Exception
- SomeException - Root of all exceptions
- IOError / IOException
- AsyncException
- StackOverflow
- HeapOverflow
- ThreadKilled
- ThreadKilled
- ArithException
- Overflow
- Underflow
- LossOfPrecision
- DivideByZeror
- Denormal
- RatioZeroDenominator
- ArrayException
- AssertionFailed
See also:
- Exceptions Best Practices - School of Haskell | School of Haskell
- Catching all exceptions - School of Haskell | School of Haskell
- Haskell/Libraries/IO - Wikibooks, open books for an open world
- 8 ways to report errors in Haskell revisited : Inside 736-131
- 8 ways to report errors in Haskell | Random Hacks
- Exception - HaskellWiki
- Coding like a drunk - Catching Exceptions in Haskell
Papers:
- Marlow, S. - An Extensible Dynamically-Typed Hierarchy of Exceptions. Available at http://community.haskell.org/~simonmar/papers/ext-exceptions.pdf
1.3.2 Examples of Exceptions:
> div 10 0 *** Exception: divide by zero > > > div 10 0 *** Exception: divide by zero > head [] *** Exception: Prelude.head: empty list > let x = undefined :: Int x :: Int > x *** Exception: Prelude.undefined CallStack (from HasCallStack): error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err undefined, called at <interactive>:1741:9 in interactive:Ghci116 > > readFile "/etc/issue" "Manjaro Linux \\r (\\n) (\\l)\n\n\n" it :: String > readFile "/etc/issuesada" *** Exception: /etc/issuesada: openFile: does not exist (No such file or directory) > > readFile "/etc/shadow" *** Exception: /etc/shadow: openFile: permission denied (Permission denied) > > readFile "/etc/" *** Exception: /etc/: openFile: inappropriate type (is a directory) > > readFile "/etcsad/" *** Exception: /etcsad/: openFile: does not exist (No such file or directory) > > > import qualified System.Directory as D > D.createDirectory "/etc/fstab/my-directory" *** Exception: /etc/fstab/my-directory: createDirectory: inappropriate type (Not a directory) > D.createDirectory "/dev/sda1/dummy" *** Exception: /dev/sda1/dummy: createDirectory: inappropriate type (Not a directory) > D.createDirectory "/i dont have permission" *** Exception: /i dont have permission: createDirectory: permission denied (Permission denied) >
1.3.3 Catching Exceptions with catch
1.3.3.1 Overview
catch <computation which may fail> <exception handler>
- Exception e
> IO a -> (e -> IO a) -> IO a
1.3.3.2 Catching all exceptions
import Control.Exception import Data.Typeable (typeOf) > :t catch catch :: Exception e => IO a -> (e -> IO a) -> IO a > > :t catch (return $ Just $ div 10 0) (\(SomeException e) -> print e >> return Nothing) catch (return $ Just $ div 10 0) (\(SomeException e) -> print e >> return Nothing) :: Integral a => IO (Maybe a) > catch (return $ Just $ div 10 0) (\(SomeException e) -> print e >> return Nothing) Just *** Exception: divide by zero > > :t catch (return $ Just $ div 10 0) (\(SomeException e) -> print e >> return Nothing) catch (return $ Just $ div 10 0) (\(SomeException e) -> print e >> return Nothing) :: Integral a => IO (Maybe a) > > catch (print $ div 10 0) (\(SomeException e) -> print $ e) divide by zero it :: () > > :t catch (print $ div 10 0) (\(SomeException e) -> print $ e) catch (print $ div 10 0) (\(SomeException e) -> print $ e) :: IO () > > catch (print $ div 10 0) (\(SomeException e) -> print $ typeOf e) ArithException it :: () > > :t catch (print $ div 10 0) (\(SomeException e) -> print $ typeOf e) catch (print $ div 10 0) (\(SomeException e) -> print $ typeOf e) :: IO () > :{ testExceptionType :: IO () -> IO () testExceptionType thunk = catch thunk handler where -- Catch All Exceptions -- It is not recommended in real life. handler :: SomeException -> IO () handler (SomeException e) = putStrLn $ "I caught an exception.\nMessage = " ++ show e ++ "\nType of exception = " ++ show (typeOf e) :} > testExceptionType (print $ div 10 0) I caught an exception. Message = divide by zero Type of exception = ArithException it :: () > > testExceptionType (error "Fatal kernel error") I caught an exception. Message = Fatal kernel error CallStack (from HasCallStack): error, called at <interactive>:82:20 in interactive:Ghci13 Type of exception = ErrorCall it :: () > > testExceptionType (readFile "/etc/shadow" >>= putStrLn) I caught an exception. Message = /etc/shadow: openFile: permission denied (Permission denied) Type of exception = IOException it :: () > > testExceptionType (readFile "/etc/" >>= putStrLn) I caught an exception. Message = /etc/: openFile: inappropriate type (is a directory) Type of exception = IOException it :: () > > testExceptionType (readFile "/" >>= putStrLn) I caught an exception. Message = /: openFile: inappropriate type (is a directory) Type of exception = IOException it :: () > > testExceptionType (ioError $ userError "I am throwing an Exception") I caught an exception. Message = user error (I am throwing an Exception) Type of exception = IOException it :: () > > testExceptionType (print $ head [1, 2, 3]) 1 it :: () > testExceptionType (print $ head []) I caught an exception. Message = Prelude.head: empty list Type of exception = ErrorCall it :: () > > testExceptionType (putStrLn "Insert a line" >> getLine >>= putStrLn) Insert a line some line some line it :: () -- User enter Ctrl + C to cancel the current input. -- > testExceptionType (putStrLn "Insert a line" >> getLine >>= putStrLn) Insert a line ^CI caught an exception. Message = user interrupt Type of exception = SomeAsyncException it :: () > > let x = undefined :: String x :: String > > testExceptionType (putStrLn x) I caught an exception. Message = Prelude.undefined CallStack (from HasCallStack): error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err undefined, called at <interactive>:103:9 in interactive:Ghci21 Type of exception = ErrorCall it :: () >
1.3.3.3 Narrowing Exceptions
Catches only arithmetic exceptions while ignoring other types of exceptions.
:{ testException :: IO () -> IO () testException ioAction = catch ioAction handler where handler :: ArithException -> IO () handler e = putStrLn $ "I got an Arithmetic exception which message is = " ++ show e :} > testException (print $ div 100 10) 10 it :: () > {-- It only will handle Arithmetic exception ignoring all other types of Exceptions. -} > testException (print $ div 100 0) I got an Arithmetic exception which message is = divide by zero it :: () > > testException (error "Unrecoverable exception.") *** Exception: Unrecoverable exception. CallStack (from HasCallStack): error, called at <interactive>:60:16 in interactive:Ghci9 > > testException (readFile "/etc/DoesntExist" >>= putStrLn) *** Exception: /etc/DoesntExist: openFile: does not exist (No such file or directory) > {- Narrow Arithmetic Exceptions -}^ :{ testArithExceptions :: IO () -> IO () testArithExceptions thunk = catch thunk handler where handler :: ArithException -> IO () handler e = case e of Overflow -> putStrLn "I got an Arithmetic / Overflow Exception" Underflow -> putStrLn "I got an Arithmetic / Underflow Exception" DivideByZero -> putStrLn "I got an Arithmetic exception / DvideByZero." _ -> putStrLn "FIXME: I don't know how to handle this exception." :} > testArithExceptions (print $ 1000 `div` 10) 100 it :: () > > testArithExceptions (print $ 1000 `div` 0) I got an Arithmetic exception / DvideByZero. it :: () > > testArithExceptions (throw DivideByZero ) I got an Arithmetic exception / DvideByZero. it :: () > > testArithExceptions (throw Overflow) I got an Arithmetic / Overflow Exception it :: () > > testArithExceptions (throw Underflow) I got an Arithmetic / Underflow Exception it :: () > > testArithExceptions (throw LossOfPrecision ) FIXME: I don't know how to handle this exception. it :: () > > testArithExceptions (throw RatioZeroDenominator) FIXME: I don't know how to handle this exception. it :: () > > testArithExceptions (readFile "/etc/issue" >>= putStrLn) Manjaro Linux \r (\n) (\l) it :: () > > testArithExceptions (readFile "/etc/issuesad" >>= putStrLn) *** Exception: /etc/issuesad: openFile: does not exist (No such file or directory) > > testArithExceptions (readFile "/etc/shadow" >>= putStrLn) *** Exception: /etc/shadow: openFile: permission denied (Permission denied) > :{ testIOExceptions :: IO () -> IO () testIOExceptions thunk = catch thunk handler where -- IOError is an alias to IOException handler :: IOError -> IO () handler e = putStrLn $ "I got an IO exception.\nMessage is =" ++ show e :} > testIOExceptions (readFile "/proc/version" >>= putStrLn) Linux version 4.4.21-1-MANJARO (builduser@manjaro) (gcc version 6.2.1 20160830 (GCC) ) #1 SMP PREEMPT Thu Sep 15 19:16:23 UTC 2016 it :: () > > > testIOExceptions (readFile "/proc/91" >>= putStrLn) I got an IO exception. Message is =/proc/91: openFile: inappropriate type (is a directory) it :: () > > testIOExceptions (readFile "/proc/" >>= putStrLn) I got an IO exception. Message is =/proc/: openFile: inappropriate type (is a directory) it :: () > > testIOExceptions (readFile "/etc/shadow" >>= putStrLn) I got an IO exception. Message is =/etc/shadow: openFile: permission denied (Permission denied) it :: () > > testIOExceptions (error "Raising an error") *** Exception: Raising an error CallStack (from HasCallStack): error, called at <interactive>:221:19 in interactive:Ghci39 > > testIOExceptions (throw DivideByZero ) *** Exception: divide by zero > > testIOExceptions (print $ div 10 0) *** Exception: divide by zero > > testIOExceptions (putStr undefined ) *** Exception: Prelude.undefined CallStack (from HasCallStack): error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err undefined, called at <interactive>:235:26 in interactive:Ghci40 > > >
1.3.3.4 Narrowing IO Exceptions
The IO Exception type is an opaque type or abstract type which the implementation or type constructors are not exposed like ArithException, therefore the only way to find the type of IO exception is by using IO Exception predicates available at System.IO.Error module.
import Control.Exception import System.IO.Error > > :t catch catch :: Exception e => IO a -> (e -> IO a) -> IO a > > :t throw throw :: Exception e => e -> a > > :t throwIO throwIO :: Exception e => e -> IO a > {- IOError is an alias for IO Exception -} > :info IOError type IOError = IOException -- Defined in ‘GHC.IO.Exception’ > > :t ioError ioError :: IOError -> IO a > -- Pasting it in the repl: {- The function ioError is used to throw a non-caught Exception inside an Exception handler. because this exception handler catches all IO exceptions; -} :{ catch (readFile "/etc/" >>= putStrLn) (\e -> case e of _ | isAlreadyExistsError e -> putStrLn "Error: File alredy exists" _ | isEOFError e -> putStrLn "Error: End of file" _ | isUserError e -> putStrLn "Error: User raised an exception" _ | isPermissionError e -> putStrLn "Error: We don't have permission to read this file" _ -> putStrLn "Uncaught exception" >> ioError e ) :} > :{ - catch (readFile "/etc/" >>= putStrLn) - (\e -> case e of - _ | isAlreadyExistsError e -> putStrLn "Error: File alredy exists" - _ | isEOFError e -> putStrLn "Error: End of file" - _ | isUserError e -> putStrLn "Error: User raised an exception" - _ | isPermissionError e -> putStrLn "Error: We don't have permission to read this file" - _ -> putStrLn "Uncaught exception" >> ioError e - ) - :} Uncaught exception *** Exception: /etc/: openFile: inappropriate type (is a directory) > :{ ioExceptionTester :: IO () -> IO () ioExceptionTester thunk = catch thunk handler where handler :: IOError -> IO () handler e = do putStrLn $ "Exception message = " ++ show e case e of _ | isAlreadyExistsError e -> putStrLn "Error: Already Exists" _ | isDoesNotExistError e -> putStrLn "Error: Doesn't exists" _ | isEOFError e -> putStrLn "Error: End of file" _ | isIllegalOperation e -> putStrLn "Error: Illegal operation" _ | isPermissionError e -> putStrLn "Error: Permission error" _ | isUserError e -> putStrLn "Error: User error" _ -> do putStrLn "Error: I can't handler this type of error." ioError e -- Raise uncaught exception :} > ioExceptionTester (readFile "/etc/issue" >>= putStrLn) Manjaro Linux \r (\n) (\l) it :: () > > ioExceptionTester (readFile "/etc/issuesddfs" >>= putStrLn) Exception message = /etc/issuesddfs: openFile: does not exist (No such file or directory) Error: Doesn't exists it :: () > > ioExceptionTester (readFile "/etc/" >>= putStrLn) Exception message = /etc/: openFile: inappropriate type (is a directory) Error: I can't handler this type of error. *** Exception: /etc/: openFile: inappropriate type (is a directory) > > ioExceptionTester (readFile "/etc/shadow" >>= putStrLn) Exception message = /etc/shadow: openFile: permission denied (Permission denied) Error: Permission error it :: () > > import qualified System.Directory as D D.createDirectory :: FilePath -> IO () > > D.createDirectory "/test" *** Exception: /test: createDirectory: permission denied (Permission denied) > > ioExceptionTester $ D.createDirectory "/test" Exception message = /test: createDirectory: permission denied (Permission denied) Error: Permission error it :: () > > ioExceptionTester $ D.createDirectory "/tmp/test" it :: () > ioExceptionTester $ D.createDirectory "/tmp/test" Exception message = /tmp/test: createDirectory: already exists (File exists) Error: Already Exists it :: () > > ioExceptionTester $ D.createDirectory "/dev/test" Exception message = /dev/test: createDirectory: permission denied (Permission denied) Error: Permission error it :: () > ioExceptionTester $ D.createDirectory "/dev/sda1" Exception message = /dev/sda1: createDirectory: already exists (File exists) Error: Already Exists it :: () > > ioExceptionTester $ throw (userError "I am raising an Error") Exception message = user error (I am raising an Error) Error: User error it :: () > > {--============= It doesn't catch non IO exceptions ============== -} > ioExceptionTester (print $ div 10 0) *** Exception: divide by zero > > ioExceptionTester (putStrLn undefined) *** Exception: Prelude.undefined CallStack (from HasCallStack): error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err undefined, called at <interactive>:310:29 in interactive:Ghci41 > > ioExceptionTester (print (read "100" :: Int)) 100 it :: () > ioExceptionTester (print (read "10asd0" :: Int)) *** Exception: Prelude.read: no parse > > ioExceptionTester $ throw Underflow *** Exception: arithmetic underflow > > > ioExceptionTester $ throw DivideByZero *** Exception: divide by zero --- Async Exception - User interrupt > ioExceptionTester (putStrLn "Enter something" >> getLine >>= putStrLn) Enter something ^CInterrupted. >
1.3.4 Catching Exceptions with catchIOError
The function catchIOError :: IO a -> (IOError -> IO a) -> IO a
from
module System.IO.Error is similar to the function catch, however it
only handles IOError(alias to IOExceptions).
import Control.Exception import System.IO.Error :{ ioExceptionTester thunk = catchIOError thunk handler where handler e = do putStrLn $ "Exception message = " ++ show e case e of _ | isAlreadyExistsError e -> putStrLn "Error: Already Exists" _ | isDoesNotExistError e -> putStrLn "Error: Doesn't exists" _ | isEOFError e -> putStrLn "Error: End of file" _ | isIllegalOperation e -> putStrLn "Error: Illegal operation" _ | isPermissionError e -> putStrLn "Error: Permission error" _ | isUserError e -> putStrLn "Error: User error" _ -> do putStrLn "Error: I can't handler this type of error." ioError e -- Raise uncaught exception :} > ioExceptionTester (readFile "/etc/issue" >>= putStrLn) Manjaro Linux \r (\n) (\l) it :: () > ioExceptionTester (readFile "/etc/issueasd" >>= putStrLn) Exception message = /etc/issueasd: openFile: does not exist (No such file or directory) Error: Doesn't exists it :: () > ioExceptionTester (readFile "/etc/" >>= putStrLn) Exception message = /etc/: openFile: inappropriate type (is a directory) Error: I can't handler this type of error. *** Exception: /etc/: openFile: inappropriate type (is a directory) > > > ioExceptionTester (readFile "/dev/sda1" >>= putStrLn) Exception message = /dev/sda1: hGetContents: invalid argument (invalid byte sequence) Error: I can't handler this type of error. *** Exception: /dev/sda1: hGetContents: invalid argument (invalid byte sequence) > > ioExceptionTester (readFile "/dev/tty0" >>= putStrLn) Exception message = /dev/tty0: openFile: permission denied (Permission denied) Error: Permission error it :: () > > ioExceptionTester (readFile "/dev/shadow" >>= putStrLn) Exception message = /dev/shadow: openFile: does not exist (No such file or directory) Error: Doesn't exists it :: () > > ioExceptionTester (readFile "/etc/shadow" >>= putStrLn) Exception message = /etc/shadow: openFile: permission denied (Permission denied) Error: Permission error it :: () > > ioExceptionTester (throw $ userError "I raised an Exception") Exception message = user error (I raised an Exception) Error: User error it :: () > {- ===== The exceptions below aren't caught by catchIOError ========== -} > ioExceptionTester (print $ div 10 0) *** Exception: divide by zero > > ioExceptionTester (print $ div 10 0) *** Exception: divide by zero > > let x = undefined - x :: a > ioExceptionTester (print $ x) *** Exception: Prelude.undefined CallStack (from HasCallStack): error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err undefined, called at <interactive>:51:5 in interactive:Ghci4 > > ioExceptionTester (error "Some error") *** Exception: Some error CallStack (from HasCallStack): error, called at <interactive>:55:20 in interactive:Ghci5 > > ioExceptionTester (throw DivideByZero ) *** Exception: divide by zero > ioExceptionTester (throw Underflow ) *** Exception: arithmetic underflow >
1.3.5 Catching Exceptions using the function catches
> import System.IO.Error > import Control.Exception > > :set -XScopedTypeVariables > > :t catches catches :: IO a -> [Handler a] -> IO a > > :info Handler data Handler a where Handler :: Exception e => (e -> IO a) -> Handler a -- Defined in ‘Control.Exception’ instance Functor Handler -- Defined in ‘Control.Exception’ > let arithmeticHandler = putStrLn "Error I got an Arithmetic exception." :{ ioExceptionHandler e | isAlreadyExistsError e = putStrLn "IO Exception - Already exists error" | isUserError e = putStrLn "IO Exception - User Error" | otherwise = do putStrLn "IO Exception - I don't know how to handle this exception" throw e :} ioExceptionHandler :: IOError -> IO () > :{ testCatches :: IO () -> IO () testCatches thunk = catches thunk handlers where handlers = [ Handler $ \ (e :: ArithException) -> arithmeticHandler , Handler $ \ (e :: IOError) -> ioExceptionHandler e , Handler $ \ (e :: ErrorCall) -> putStrLn "I got an ErrorCall exception" , Handler $ \ (e :: SomeException) -> do putStrLn "I don't know how to handle this exception" throw e ] :} > > > testCatches (throw DivideByZero) Error I got an Arithmetic exception. it :: () > testCatches (throw Overflow ) Error I got an Arithmetic exception. it :: () > testCatches (let x = undefined in print x) I got an ErrorCall exception it :: () > testCatches (error "Some Error") I got an ErrorCall exception it :: () > testCatches (readFile "/etc/issue" >>= putStrLn) Manjaro Linux \r (\n) (\l) it :: () > testCatches (readFile "/etc/issuesf" >>= putStrLn) IO Exception - I don't know how to handle this exception *** Exception: /etc/issuesf: openFile: does not exist (No such file or directory) > > testCatches (throw $ userError "throw my exception") IO Exception - User Error it :: () > >