Better to Ask Forgiveness

Exception Lore

The received wisdom is that you should avoid using exceptions for the main logic of your program. They’re simply there so you don’t have to obfuscate your normal code paths with error checking at every line.

I think this is generally true. Exceptions are a sharp implement: a non-local transfer of control, a standin for goto cascades, and just a rung down from the Lovecraftian madness of full continuations. Anything you use them for is going to require extra work to comprehend.

I’d even like to propose a test for whether you’re over-utilizing exceptions: could you remove all of the exception-handling code in your program without affecting its normal operation? Assuming you weren’t using exceptions totally gratuitously, you’d be impacting robustness (and in that sense, correctness), but that’s not the point. Just: would the program still work under normal conditions and across the entire possible range of inputs?

However, there is one important place where this rule can’t apply, and sadly it revolves around the one example of don’t-use-exceptions-in-business-logic that almost everyone gives: testing for non-existence of a file before opening it.

Proving the Rule: A Dialogue

I’ve been corrected, more than once, for writing code like this:

begin
  File.open filename do |stream|
    # do stuff with stream
  end
rescue Errno::ENOENT
  # do other stuff if file doesn't exist
end

According to my interlocutors, I’ve got my exceptions in my business logic here. Instead, I should have written:

if File.exists? filename
  File.open filename do |stream|
    # do stuff with stream
  end
else
  # do other stuff if file doesn't exist
end

To which I always respond, “Nice race condition.”

“Race condition?”

“Yes. What happens if the file is deleted after File.exists?, but before File.open ?”

“That’s not likely to happen. Besides, the window’s really small.”

“Yes, very small. What if they’re lucky?”

“The user isn’t going to be deleting files in the middle of this.”

“Can you guarantee that? You’d get an uncaught exception, and the user might lose their work.” Actually, I’ve been bitten by exactly this sort of bug once when I had a shell rm -r ‘ing files in the background.

My interlocutor finally caves. “Okay, fine. How’s this?”

if File.exists? filename
  begin
    File.open filename do |stream|
      # do stuff with stream
    end
  rescue Errno::ENOENT
    # do other stuff if file doesn't exist
  end
else
  # do other stuff if file doesn't exist
end

“Tch! You’re repeating yourself. Besides, isn’t that an exception in your business logic?”

“Umm… yeah. Okay, how’s this?”

exists = false
if File.exists? filename
  begin
    File.open filename do |stream|
      # do stuff with stream
    end
    exists = true
  rescue Errno::ENOENT
  end
end

unless exists
  # do other stuff if file doesn't exist
end

“Great. Much clearer, isn’t it?”

“Not really.”

“Well, what if you turned the problem inside-out? Put the test inside the exception handler and raised an exception as needed?”

“You mean like this?”

begin
  if File.exists? filename
    File.open filename do |stream|
      # do stuff with stream
    end
  else
    raise Errno::ENOENT
  end
rescue Errno::ENOENT
  # do other stuff if file doesn't exist
end

“That’s right. Do you really need to test File.exists? in there though?”

Forgiveness and Permission

Of course, my preferred approach isn’t without some of the usual disadvantages of using exceptions this way; one often overlooked one is that you need to be aware of any possibility that your do stuff with stream code could itself raise an Errno::ENOENT . In those cases, you have to be prepared to distinguish that event from filename not existing. Still, you don’t have many other options in this case.

A lot of the reason for this is that most filesystem APIs suck: they should be transactional, so you could just test File.exists? and not have to worry about some other process yanking the carpet out from under you before you have a chance to call File.open . However, there are other practical problems that have nothing to do with concurrency.

Among them is the matter of file permissions. I’ve personally been the victim of code which (essentially) assumes that File.writable? would accurately reflect whether a file is really writable. This is true in trivial cases, but once you throw network filesystems into a heterogenous environment things start to break down. You often end up situations where explicit permissions tests are performed by one party (generally the client) examining the ACL, but actual enforcement is handled by the other (hopefully the server). If they are running different operating systems (or even if the filesystem is sufficiently exotic that it doesn’t mesh well with the client OS’s access control scheme), they may not agree on who has what rights.

There is nothing more maddening than getting helpdesk calls from users who report they don’t have access to their own files, when it’s really some brain-dead closed-source C++ application program explicitly testing permissions and not even bothering to open the bloody file because it assumes it can’t.

When the client and server disagree about access rights, if you go ahead and try the operation without checking beforehand, you’ll at least be siding with the one that really matters. When it comes to the filesystem, it’s far better to catch an exception and ask forgiveness than it is to explicitly test permission beforehand.

hoodwink.d enhanced