Errors and exceptions
| Partial functions | |
| throw and catch | |
| Exception types | |
| Which error style |
Partial functions
A function that crashes on some inputs is partial. The two standard ways to crash are error for a real bug and undefined for a placeholder:
head [] -- exception: empty list
error "unreachable" -- exception with a message
undefined -- crashes when forced
undefined is useful while writing a skeleton, because the program compiles and fails only where the hole is actually evaluated. Leaving one in production is the classic Haskell incident.
throw and catch
Exceptions in IO are caught by type. A missing file raises an IOException, and catch handles it:
import Control.Exception
safeRead :: FilePath -> IO (Maybe String)
safeRead path =
(Just <$> readFile path)
`catch` \e -> do
hPutStrLn stderr (show (e :: IOException))
pure Nothing
The type annotation on e is required, because the handler has to say which exceptions it claims to understand. A handler that catches everything is a bug waiting to happen.
Exception types
Any type can be thrown if it implements Exception. For application errors a small sum type is clearer than a string:
data TodoError
= NotFound Int
| BadInput String
deriving (Show)
instance Exception TodoError
removeItem i items
| i < 0 = throwIO (BadInput "negative index")
| otherwise = deleteAt i items
The empty instance is enough, because the class provides the rest. Now a caller can catch TodoError without catching a disk failure by accident.
Which error style
Three styles coexist for good reasons. Pick by where the decision belongs:
-- expected failure, caller decides: Either
parse :: String -> Either String Int
-- absent value, no reason needed: Maybe
find :: Int -> [Int] -> Maybe Int
-- environmental failure, cleanup needed: IO exceptions
readConfig :: FilePath -> IO Config
error is reserved for broken invariants that
the type system cannot express. If a caller could recover, the
signature should say so.
Next:
Concurrency
Article author: Arthur Isaev