Preface
Ruby exceptions and error handling is not the most remarkable feature of the Ruby language. Infact, the way Ruby deals with exceptions is strikingly similar to other languages (such as Java etc.). But, I think all the stuff I’ve been writing about Ruby lately has taken on a life of it’s own for me :), so I am going to quickly go over this topic if only for completeness sakes.
Raising Exceptions
Getting Ruby to raise an exception is easy :). Ruby, like many other languages has a hierarchy of exception classes (that all inherit from the class Exception), some of these are familiar and fairly easy to produce, for example ZeroDivisionError or NoMethodError. Here is some code which will do just that:
Of course you don’t have to wait for Ruby to raise exceptions for you, you can do so explicitly in your code with the raise keyword (it is actually a method). Lets write a method where we explicitly raise an exception if the argument we pass in is false:
- def i_must_have_truth(value)
- raise TypeError, 'You must give me truth' if value == false
- end
- i_must_have_truth false
As you can see we are able to raise a specific exception and pass it a message which then gets printed out to the console. There is also a handy shortcut, if you use raise without giving it a specific exceptions (or even without giving it an error message to display), Ruby will automatically raise a RuntimeError for you which is pretty handy:
- def i_must_have_truth(value)
- raise "Hello"
- end
- i_must_have_truth false
Rescuing Exceptions
So far so good, but life would be pretty tough if we couldn’t handle the exceptions that get thrown in any way, This is where the rescue clause comes in. If we wrap some code in a begin .. end block and put a rescue clause in the middle of that, control will go to the rescue clause if any exception is thrown by the code. Let us demonstrate:
- begin
- 1/0
- p 'I should never get executed'
- rescue
- p 'I am rescuing an exception and can do what I want!'
- end
As you can see the first string does not get printed since the division by zero will throw an exception and control will pass to the rescue clause, which will print out the second string for us. Using rescue by itself will allow you to rescue all exceptions that get thrown by the code, but sometimes you might want to only capture specific ones and let other code handle all the others. The rescue clause allows you to do this as well:
- i=0
- while i<=10
- begin
- if i ==0
- 1/0
- end
- raise "random runtime exception"
- p 'I should never get executed'
- rescue ZeroDivisionError
- p 'I am rescuing only ZeroDivisionErrors!'
- i+=1
- end
- end
As you can tell, we rescued the first exceptions since it was a division by zero, but the second exception does not get rescued and so the program exits with an error. However, we sometimes want to execute some code regardless of whether an exception was thrown or not (i.e. we may want to do some cleanup). Java has the finallykeyword for this, Ruby has ensure. We can put an ensure clause within out begin .. end block. The code inside this clause gets executed regardless of whether the code throws an exception or not. For example, if we are reading from a file, we want to close the file handle no matter if an exception is thrown or not, we can do the following:
- file = nil
- begin
- file = File.open("blah.txt")
- raise
- rescue
- p 'I rescue all exception and raise new ones'
- ensure
- file.close if file!=nil
- p 'just closed the file'
- end
Even though an exception was thrown and rescued, the code in the ensure clause executes regardless.
Rescuing Exceptions Inside Methods
If we are inside a method and we want to rescue some exceptions, we don’t actually need a begin .. end block since the method definition itself will act in that capacity. So, we can do something like this:
- def some_method
- p 'Hello method'
- raise
- p 'Bye method'
- rescue
- p 'Rescuing exceptions'
- end
- some_method
We have rescued an exceptions without having a begin .. end block.
You are not limited to knowing just the type of the exception you’re rescuing, you can get more information at your disposal. When you rescue an exception you can get a hold of the actual exception object that was thrown and then query this object for various details. Here is how:
- begin
- raise ZeroDivisionError, "Hello I am a random zero division error"
- rescue ZeroDivisionError => e
- p e.message
- p e.backtrace
- end
If we execute the above code we will print out the message as well as the stack trace of the exception we rescued which are provided by the exception object itself. You can also do the same thing with a general rescue clause:
- begin
- raise "Hello I am a random runtime error"
- rescue => e
- p e.message
- p e.backtrace
- end
Creating Your Own Exceptions
Creating your own exceptions in Ruby is extremely simple, all you need to do is create a class that inherits from Exception or one of it’s descendants and you’re good to go:
- class MyCrazyException < Exception
- end
- raise MyCrazyException, "I am a crazy new exception"
As you can see Ruby is saying that it has raised our new exception rather than one of it’s regular ones. As usual, this allows you to define different types of exceptions for various error conditions in your code which also allows you to rescue them by name. You may or may not want to do this, you can easily get away with just using runtime exceptions but it is up to you.
Update.
The Ruby Exception Hierarchy
If you’re curious about the kinds of exceptions that Ruby has predefined for you, here is a list:
The only thing that is missing from that list is the Errno family of exceptions. These are a whole set of exceptions that relate to file I/O and live in the Errno namespace. You can see one of these in action if you try to open a file that doesn’t exist, e.g.:
- File.open("")
You’re now a Ruby exception guru, go forth and use your new Ruby error handling powers for good instead of evil :).
Supplement
* Ruby 手冊 - 救援 Exception processing: rescue
* Ruby 手冊 - 確認 Exception processing: ensure
沒有留言:
張貼留言