Finer-grained file system exceptions

Looking at 15.2 Filesystem , most if not all file operations, if they fail, raise exn:fail:filesystem. However, what should I do if I need more detailed information?

For example, if I call rename-file-or-directory and an exn:fail:filesystem is raised, how could I tell why this call failed? (Was it because the source file wasn't found, was it because I didn't have write permissions for the target directory or something else?)

Some languages (notably C) use error codes to check after the call, which is a bit awkward, but at least some more detailed information is accessible. Another approach would be to raise more specific exceptions for specific error types. (Incidentally, Python used to use only error codes, but later added specific exceptions.) Would using more specific exceptions be a feasible approach for Racket, too?

1 Like

However, what should I do if I need more detailed information?

(with-handlers ((exn:fail? displayln)) 
   (rename-file-or-directory "c:/non-existent" "c:/something-else"))
#(struct:exn:fail:filesystem:errno rename-file-or-directory: cannot rename file or directory
  source path: c:\non-existent
  dest path: c:\something-else
  system error: The system cannot find the file specified.; win_err=2 #<continuation-mark-set> (2 . windows))
1 Like

Yes, printing the information from a failure is what I'm usually doing for now, so the user can react to that. However, this doesn't help if you could handle certain error situations (but not others) programmatically.

So far, I've gotten away with the approach you mentioned, but it feels limited to me.

Another workaround would be to implement extra checks to distinguish error situations, but this is more code to write and possibly debug, and it's subject to race conditions.

1 Like

Perhaps my original message was not clear. The exception raised is actually exn:fail:filesystem:errno which contains the error code, which is platform specific, so you can filter by that:

> (with-handlers
    ((exn:fail:filesystem:errno?
      (lambda (e)
        (match-define (cons errno platform) (exn:fail:filesystem:errno-errno e))
        (printf "Call failed with error code ~a, on platform ~a~" errno platform))))
  (rename-file-or-directory "c:/non-existent" "c:/something-else"))
Call failed with error code 2, on platform windows

Or you can catch specific exceptions:

(with-handlers
    (((lambda (e)
        (and (exn:fail:filesystem:errno? e)
             (equal? '(2 . windows) (exn:fail:filesystem:errno-errno e))))
      (lambda (e)
        (printf "We only catch windows errors of type \"The system cannot find the file specified\"~%"))))
  (rename-file-or-directory "c:/non-existent" "c:/something-else"))
We only catch windows errors of type "The system cannot find the file specified"

Although you are correct that the exception does not contain the system call that failed and its parameters (which are present in the actual exception message.

Alex.

1 Like

Ok, this is clearer, thank you! :slight_smile:

I wonder though how usable and reliable this is in practice.

  • The documentation of exn:fail:filesystem:errno only says "Raised for a filesystem error for which a system error code is available." So alone from the documentation, there's no information which specific functions would raise exn:fail:filesystem:errno and which functions would raise just exn:fail:filesystem. Even if I found by trial and error that a function raises exn:fail:filesystem:errno, I wouldn't be sure if that's the case on other platforms or on my development platform in the future.

    I also checked the Racket source code and found only a few places where the error number is actually used, mostly for deletion and renaming operations on Windows. I didn't find the place where exn:fail:filesystem:errno exceptions are actually raised. I guess that's somewhere in the kernel code.

  • It's not clear what the specific error codes for different conditions are. I guess for Windows these are used. Under Posix, I guess what's meant are the constants from errno.h.

  • Since the errno values are platform-specific, it'll be kind of tedious to handle error codes for different platforms in the Racket code.

  • There's no uniform mapping of errno numeric constants and symbolic constants (semantics). Even when comparing the values for the "Posix" platforms Linux (errno-base.h and errno.h), FreeBSD and Mac OS X/Darwin, many of the symbolic/numeric pairs are the same, but there are also differences. For example, Linux's EAGAIN has a value of 11, whereas the same error constant in FreeBSD and Darwin has a value 35.

Enough ranting. :wink: I'm not sure what should be done. Given limited resources (as usual), I can imagine to put in the work to define symbolic constants that are uniform for all supported platforms isn't tempting, although I think that would be the "correct" thing to do. (I believe Python does this, even trying to define common symbolic constants for Posix and Windows.) If someone wants to implement a mapping from numeric to symbolic errno constants and back, the source in each case should be the errno.h header file for each platform. For reliable APIs, it should be documented which functions are guaranteed to raise exn:fail:filesystem:errno.

While writing this message, I noticed that the matter is surprisingly convoluted and that the research took much more time than expected. So at some point I thought of just stopping the research and discarding the message. But maybe it's still helpful, so I finished the message. Maybe someone wants to work on this at some point.

2 Likes

The lookup-errno function maps errno symbolic names to integers.

2 Likes

Awesome, thanks! :slight_smile:

I wonder if it also maps to Windows error codes on Windows.