8

I'm writing an application in Haskell and would like to display a meaningful error message to the user if readFile or writeFile fails. I'm currently catching IOErrors with Control.Exception.tryJust and converting them to human-readable text.

However, I'm having trouble figuring out which errors I should catch and how to extract information from them. For example, assuming "/bin" is a directory and "/bin/ls" is a file, readFile "/bin" and readFile "/bin/ls/asdf" both give "inappropriate type" but (in my opinion) they are different errors. In the case of the first one, I could recover by processing each file within the directory, whereas the second is more like a "does not exist" type of error.

In relation to the previous example, there doesn't seem to be a portable way of catching "inappropriate type" errors. Looking at GHC.IO.Exception, InappropriateType is marked GHC-only so I can't just pattern match on ioeGetErrorType. I could pattern match on ioeGetErrorString but I'm not sure if those strings are always the same across different platforms, compilers, locales, etc.

In summary, my questions are:

  1. Which exceptions should I be catching for readFile/writeFile?
  2. Once I have an exception, how should I go about extracting information from it?
  3. Is there a portable way of catching the GHC-only exceptions such as InappropriateType?

Update:

Based on @ErikR's answer I'm looking at the fields of GHC.IO.Exception.IOException with the following Haskell program:

import Control.Exception (try)
import GHC.IO.Exception (IOException(..))
import qualified Data.ByteString as B


main :: IO ()
main = do
    try (readFile "/nonexistent") >>= printException
    try (writeFile "/dev/full" " ") >>= printException
    try (readFile "/root") >>= printException
    try (readFile "/bin") >>= printException
    try (writeFile "/bin" "") >>= printException
    try (readFile "/bin/ls/asdf") >>= printException
    try (writeFile "/bin/ls/asdf" "") >>= printException
    try (B.readFile "/dev/null") >>= printException

    -- I have /media/backups mounted as read-only. Substitute your own read-only
    -- filesystem for this one
    try (writeFile "/media/backups/asdf" "") >>= printException

printException :: Either IOError a -> IO ()
printException (Right _) = putStrLn "No exception caught"
printException (Left e) = putStrLn $ concat [ "ioe_filename = "
                                            , show $ ioe_filename e
                                            , ", ioe_description = "
                                            , show $ ioe_description e
                                            , ", ioe_errno = "
                                            , show $ ioe_errno e
                                            ]

The output on Debian Sid GNU/Linux with GHC 7.10.3 is:

ioe_filename = Just "/nonexistent", ioe_description = "No such file or directory", ioe_errno = Just 2
ioe_filename = Just "/dev/full", ioe_description = "No space left on device", ioe_errno = Just 28
ioe_filename = Just "/root", ioe_description = "Permission denied", ioe_errno = Just 13
ioe_filename = Just "/bin", ioe_description = "is a directory", ioe_errno = Nothing
ioe_filename = Just "/bin", ioe_description = "Is a directory", ioe_errno = Just 21
ioe_filename = Just "/bin/ls/asdf", ioe_description = "Not a directory", ioe_errno = Just 20
ioe_filename = Just "/bin/ls/asdf", ioe_description = "Not a directory", ioe_errno = Just 20
ioe_filename = Just "/dev/null", ioe_description = "not a regular file", ioe_errno = Nothing
ioe_filename = Just "/media/backups/asdf", ioe_description = "Read-only file system", ioe_errno = Just 30
Matthew
  • 193
  • 6
  • 1
    I'm not optimistic about "portable" exceptions with the current state of the Haskell infrastructure. We've been stuck in one-compiler mode for a number of years. Here's hoping that changes soon. – dfeuer Jun 28 '16 at 15:55

1 Answers1

4
  1. Which exceptions should I be catching for readFile/writeFile?

Under OS X, if you use openFile followed by hGetContents instead of readFile then you will get different exceptions for the cases you mention.

openFile "/bin/ls/asdf" ... will throw a "no such file or directory" exception whereas openFile "/bin" ... will throw "inappropriate type".

Under Linux both open calls will throw a "inappropriate type" exception. However, you can distinguish between the two via the ioe_errno and ioe_description fields:

import System.IO
import GHC.IO.Exception
import Control.Exception

foo path = do
  h <- openFile path ReadMode
  hClose h

show_ioe :: IOException -> IO ()
show_ioe e = do
  putStrLn $ "errno: " ++ show (ioe_errno e)
  putStrLn $ "description: " ++ ioe_description e

bar path = foo path `catch` show_ioe

Sample ghci session:

*Main> bar "/bin"
errno: Nothing
description: is a directory
*Main> bar "/bin/ls/asd"
errno: Just 20
description: Not a directory
  1. Once I have an exception, how should I go about extracting information from it?

Each exception has its own structure. The definition of an IOException may be found here.

To bring the field accessors into scope you need to import GHC.IO.Exception.

  1. Is there a portable way of catching the GHC-only exceptions such as InappropriateType?

As @dfeuer said, for all practical purposes GHC is the only Haskell implementation at this time.

Update

Results from running your program. I didn't include the last result because I didn't have a read-only filesystem around to test it on, but I'm sure the error would be the same.

ioe_filename = Just "/nonexistent", ioe_description = "No such file or directory", ioe_errno = Just 2
ioe_filename = Just "/dev/full", ioe_description = "Permission denied", ioe_errno = Just 13
ioe_filename = Just "/root", ioe_description = "is a directory", ioe_errno = Nothing
ioe_filename = Just "/bin", ioe_description = "is a directory", ioe_errno = Nothing
ioe_filename = Just "/bin", ioe_description = "Is a directory", ioe_errno = Just 21
ioe_filename = Just "/bin/ls/asdf", ioe_description = "Not a directory", ioe_errno = Just 20
ioe_filename = Just "/bin/ls/asdf", ioe_description = "Not a directory", ioe_errno = Just 20
ioe_filename = Just "/dev/null", ioe_description = "not a regular file", ioe_errno = Nothing
ErikR
  • 51,541
  • 9
  • 73
  • 124
  • I still get "inappropriate type" from both using System.IO.openFile on GHC 7.10.3 for GNU/Linux. – Matthew Jun 29 '16 at 03:53
  • I guess the exact exception is OS dependent then - I tested it under OS X + GHC 7.10.2. Updated answer. – ErikR Jun 29 '16 at 04:13
  • However - you do get a slightly different message - "not a directory" vs. "is a directory" - so that is a way to distinguish between the two. – ErikR Jun 29 '16 at 04:15
  • Thank you for pointing me towards ioe_description, that ended up being exactly what I needed. I've updated my question with the strings that I'm catching on GNU/Linux but I don't have access to OS X. Could you please check that they're the same on OS X. Thanks again for the help! – Matthew Jun 29 '16 at 14:25
  • 1
    Actually I would base it on `ioe_errno` because on Unix/Linux/OS X systems the description string is a function of the errno error code and the errno codes are much more likely to be the same. If you give me a program that generates the errors I'll run it on my OS X system. – ErikR Jun 29 '16 at 15:13
  • I added a Haskell program to my question. You might need to change some of the paths since I'm not sure what OS X has under /dev/ and whether or not /root exists. – Matthew Jun 29 '16 at 16:17