The Diversity of Error Handling
written by André Gawron, 23. October 2010This is the first part of an article series about porting the Lisp's condition system to Ruby.
Have you ever been in the situation where you had to return a false for different kind of errors? Or in a worse situation of handling such kind of error? You might think: "What a dork, that's bad programming style!", or perhaps you don't care about the way the error is promoted because error handling is for pussies. But that's wrong. Good error handling is vital for a viable, stable and robust system. And it's hard. If done right though, you should be proud of yourself.
But why is error handling hard? You throw an error and catch it on a level where you think you can handle it. Sounds easy, doesn't it? Although I'm using terminology mostly used in combination with exceptions, the basic workflow also applies to other ways of promoting an error. But how many ways do you know yourself to raise an error and handling it afterwards? A ton.
return "values"
def foo
# do something useful
return false if bar.empty?
# do more
return true
end
There are return values. A lot of 'em. The easiest and most straight forward ones are true and false1. Yeah, cool. But how much information can you extract from those values? Just two cases. It worked, or it didn't2. How are you supposed to handle an error correctly, if you have no idea what happened? The remote server could have crashed, so trying to connect to it again is not a great idea; you're basically reading tea leaves. What are we looking for? Yes, we need more information.
return 42
No, it's not the answer to the everlasting question of human kind. It's a status code. What does it mean? I have no idea, I have to look it up. There are more than this one: -1, 0, 1, 425 and so on. As long as you have some kind of fetish for cryptic error message, you don't want to use those. When writing code, everything which disturbs you from thinking about the problem you are going to solve should be avoided. Looking up fancy error codes is one of those things. You have to understand errors and their meaning right off the bat, or at least it should be possible to make an educated guess. To get readable status codes, one could make use of constants. That's better, but please never be in the situation to return actual numbers or strings that interfere with your codes.
def foo
# do something useful
# readable, isn't it?
return -1 if bar.empty?
return -2 if bar.is_a?(Hash)
# do more
return 1
end
raise Exception, "reasonable error message"
Next one: exceptions. Nifty tool. I do like the idea of exceptions. You can transport information using the Exception class. You're able to split up code that's gonna handle the error and code that may gonna trigger the error. This makes the code more readable and maintainable. You can decide if you're capable of handling the error or let it slip through until somebody finally brings an end to the error nightmare.
But depending on the personal view, the nonlocal-exit and the way the exceptions propagate the callstack up can also lead to a disadvantage. It's difficult to build transaction-safe exception handling. For example, you have to be aware of opened resources and especially of the state which was changed by the working code before the exception was raised. You have to take that into account and rewind the state accordingly3.
begin
# delete an entry
Foo.delete(bar)
# save it to the database but it throws an Exception
Foo.save
# never executed
Foo.refresh
rescue Exception => ex
# entry is still deleted in Foo but not in database
# this results in data inconsistency
# between application and database
Foo.refresh
end
But exceptions do have another drawback: their name. Exceptions are - by design - exceptional. Using exceptions in every context to propagate every kind of error4 5 makes their exceptional status ridiculous. So you're ending up mixing different kinds of error handling together, creating a total hullabaloo.
To make long story short: you want a flexible condition system.
- or similar, depending on your language
- Caution! I implicitly defined the meaning of true and false. You're free to write a function which returns false if everything works smoothly
- on a little sidenote: State Is Your Enemy - more on that maybe in another post
- there are plenty definitions of error, in this context an error is everything which can go wrong
- even the expected failure of a function