Ruby Exception Handling Practice: Be As Specific As Possible


Avdi Grimm covers Ruby exception handling in way more detail in Exceptional Ruby (not an affiliate link!) and you should check it out

Ruby Exception Handling

In Ruby, you should already be aware that catching Exception is a blunt instrument that captures error states that generally should not be handled by your code. This is emphasized by the fact that rescue without an Exception class specified defaults to catching subclasses of StandardError:

Exception
├── NoMemoryError
├── ScriptError
│   ├── LoadError
│   ├── NotImplementedError
│   └── SyntaxError
├── SecurityError
├── SignalException
│   └── Interrupt
├── StandardError (default for rescue)
│   ├── ArgumentError
│   │   └── UncaughtThrowError
│   ├── EncodingError
│   ├── FiberError
│   ├── IOError
│   │   └── EOFError
│   ├── IndexError
│   │   ├── KeyError
│   │   └── StopIteration
│   ├── LocalJumpError
│   ├── NameError
│   │   └── NoMethodError
│   ├── RangeError
│   │   └── FloatDomainError
│   ├── RegexpError
│   ├── RuntimeError (default for raise)
│   │   └── FrozenError
│   ├── SystemCallError
│   │   └── Errno::* (various system error subclasses)
│   ├── ThreadError
│   ├── TypeError
│   └── ZeroDivisionError
├── SystemExit
├── SystemStackError
└── fatal (cannot be rescued)

But even in that case, you shouldn’t just rescue blindly, especially with no logging

begin
  try_something
rescue
  # i don't like exceptions
end

If you are looking for a specific outcome to handle:

begin
  try_something
rescue BadButKindOfExpected
  # this is a thing that is recoverable and we
  # occasionally get, it's no big deal
end

Or maybe you can match on the message itself because two different versions of a library raise different exceptions but a similar error message:

begin
  try_something
rescue => e
  raise unless e.message =~ /totally expected but ok/
end

But I just want to go on with my

“Ok, but all of my exceptions are things that are good,” you say. Sure. Today. But then, tomorrow someone breaks your encryption process and you end up broken data. But you silently return nil via rescues falling through to an implicit return from the method. And you have code on the receiving end that skips doing anything if the return value is nil.

Then, at some point, your decryption library stops raising that error because it was a vector for a timing attack. You’re now receiving garbage which not nil and therefore runs your code with the garbage. You have a very expensive bug to debug: one that was uncovered long after the breakage because you were rescueing the error that is no longer being thrown.

Rescue/Catch Specifically

You should probably know the exceptional cases are that decent possibilities. Rescue those specifically by class or string message match. If you must catch all, log stack traces in debug mode at a minimum. Catching errors with no repropagation or logging not only cuts you off from being aware of false negatives with regard to breakage, but also cuts you off from being able to know the actual source of the error.


Leave a Reply