To the rescue! A Ruby tip.
Posted: Tue Sep 07, 2021 3:43 pm
In a recent conversation with Spogg I mentioned a functionality in Ruby, of which he thinks not many may know and therefore asked me to write a post about it.
When writing code, it is inevitable to cause an error at some point. Any higher language can catch such errors, which means, they are brought to the programmer's attention, before they can do any harm. Those are called "exceptions" and the process of dealing with exceptions "exception handling".
As any other high level language, Ruby also has a mechanism for exception handling. It is very convenient, flexible and at the same time very easy to use.
Whenever there is critical code, of which you must ensure that it runs no matter what, the standard behavior of Ruby will prevent that, because, if an exception is raised, Ruby tells you about the error in the info pane and then stops execution.
A better way is to use the "begin end" structure. Place the code section, that is critical, in this block. As an example, we will provoke a TypeError.
Immediately, Ruby tells you that it can't convert a fixnum (3) into an array, and then stops execution. Now we have a mess, because the variable number was already instantiated, before the exception was raised, but because the execution was stopped, it is now of NilClass. Not at all what we need! A number is, what the rest of the program expects.
With this simple structure, we already managed one big step: Ruby does not print the error to the info pane and stops, but continues to run. However, under rescue, we have no code. We don't handle the exception! number is still an instance of NilClass.
The rescue keyword is important. Every exception that is raised within the begin block, will be routed to it, instead of Ruby's standard behavior. We need a way to handle the exceptions, and that is done with the exception class. It will allow us to handle an exception based on its type. Ruby already provides a fine list of exceptions that are bundled under StandardError. When we tell rescue to catch StandardError, it will catch ALL standard errors that are defined, like TypeError, NameError, ZeroDivisionError, and so many more. If you write rescue with no exception specification, Ruby assumes StandardError automatically. The following codes are the same in functionality
Let's use our rescue block now. In the simplest form, we just notify ourselves of the exception. But Ruby does that already without writing a begin end block. The next simple form would be to give a default value out, when an exception arrives.
That's better. Number is now 0 instead of nil, and the rest of the program can deal with that. Ruby continues to run flawlessly.
But there's more. Sometimes you don't want to catch ALL exceptions, as following code might already deal with some of them. In that case we just select a subclass of StandardError. In our example, it would be a TypeError, we're interested in.
Now only a type error will be caught and dealt with, but all others be raised. And you can go into much deeper detail. You can specify more than one exception to be caught in a rescue line.
Now our rescue block will react to Type and Name errors. Often times, different exceptions need different handling. No problem. Just add another rescue line (actually as many as you need).
But you didn't think that was it, did you? When rescuing an exception, it might be possible to run the code, that failed before! Therefore you use the keyword retry.
But that still isn't it. Sometimes you need to make sure that some code is run under all circumstances. It could be difficult to repeat the code in all rescue blocks. And so there is another helpful keyword: ensure. It does exactly what it says. It ensures that whatever code is under it, will be executed, EVEN IF AN EXCEPTION WAS NOT HANDLED BY OUR RESCUE CODES! This is important to understand. Ensure will run, even if Ruby has to stop after an unhandled exception. This is why many Ruby programmers use begin end to open files. It is dangerous and even destructive to open files, but not closing them after use. But an unhandled exception will lead to exactly that. It can be avoided with ensure, like so.
If now an unhandled exception is raised, the file will be closed, before Ruby stops. However, ensure will always be executed, even if there was no exception, or if all exception were handled. That means, you have to make sure, that the code under ensure will not be executed elsewhere in the begin end block! In this example the file should not be closed in the rescue block!
I still have more for you. Sometimes, instead of just defining which exceptions to catch, you may want to access the exception in the rescue block (especially useful with your own exception, see below). You do that by referencing a local variable.
Last but not least, as well as handling exceptions, you can also raise one! This is helpful for various reasons, for example to re-raise an exception that was already handled. Some higher layer code might need to be informed of the error, even if it was handled.
raise without any argument in the rescue block will re-raise the same eexception, here a ZeroDivisionError.
Here you raise a RuntimeError with the message you can read there. You can also create your own exceptions that you can then raise. Exceptions are classes, just like everything else in Ruby. Just make sure to inherit from StandardError
You can overwrite the default message as well.
I probably forgot about something. But this will give you a start into exception handling. Have fun programming!
When writing code, it is inevitable to cause an error at some point. Any higher language can catch such errors, which means, they are brought to the programmer's attention, before they can do any harm. Those are called "exceptions" and the process of dealing with exceptions "exception handling".
As any other high level language, Ruby also has a mechanism for exception handling. It is very convenient, flexible and at the same time very easy to use.
Whenever there is critical code, of which you must ensure that it runs no matter what, the standard behavior of Ruby will prevent that, because, if an exception is raised, Ruby tells you about the error in the info pane and then stops execution.
A better way is to use the "begin end" structure. Place the code section, that is critical, in this block. As an example, we will provoke a TypeError.
- Code: Select all
ary = [] #creates an array class instance
number = ary + 3
Immediately, Ruby tells you that it can't convert a fixnum (3) into an array, and then stops execution. Now we have a mess, because the variable number was already instantiated, before the exception was raised, but because the execution was stopped, it is now of NilClass. Not at all what we need! A number is, what the rest of the program expects.
- Code: Select all
ary = [] #creates an array class instance
begin
number = ary + 3
rescue
end
With this simple structure, we already managed one big step: Ruby does not print the error to the info pane and stops, but continues to run. However, under rescue, we have no code. We don't handle the exception! number is still an instance of NilClass.
The rescue keyword is important. Every exception that is raised within the begin block, will be routed to it, instead of Ruby's standard behavior. We need a way to handle the exceptions, and that is done with the exception class. It will allow us to handle an exception based on its type. Ruby already provides a fine list of exceptions that are bundled under StandardError. When we tell rescue to catch StandardError, it will catch ALL standard errors that are defined, like TypeError, NameError, ZeroDivisionError, and so many more. If you write rescue with no exception specification, Ruby assumes StandardError automatically. The following codes are the same in functionality
- Code: Select all
begin
rescue
end
- Code: Select all
begin
rescue StandardError
end
Let's use our rescue block now. In the simplest form, we just notify ourselves of the exception. But Ruby does that already without writing a begin end block. The next simple form would be to give a default value out, when an exception arrives.
- Code: Select all
ary = [] #creates an array class instance
begin
number = ary + 3
rescue
number = 0
end
That's better. Number is now 0 instead of nil, and the rest of the program can deal with that. Ruby continues to run flawlessly.
But there's more. Sometimes you don't want to catch ALL exceptions, as following code might already deal with some of them. In that case we just select a subclass of StandardError. In our example, it would be a TypeError, we're interested in.
- Code: Select all
ary = [] #creates an array class instance
begin
number = ary + 3
rescue TypeError
number = 0
end
Now only a type error will be caught and dealt with, but all others be raised. And you can go into much deeper detail. You can specify more than one exception to be caught in a rescue line.
- Code: Select all
ary = [] #creates an array class instance
begin
number = ary + 3
rescue TypeError, NameError
number = 0
end
Now our rescue block will react to Type and Name errors. Often times, different exceptions need different handling. No problem. Just add another rescue line (actually as many as you need).
- Code: Select all
ary = [] #creates an array class instance
begin
number = ary + 3
rescue TypeError, NameError
number = 0
rescue ZeroDivisionError
#some code to prevent division by zero#
end
But you didn't think that was it, did you? When rescuing an exception, it might be possible to run the code, that failed before! Therefore you use the keyword retry.
- Code: Select all
ary = [] #creates an array class instance
begin
number = ary + 3
rescue TypeError
ary = 0
retry #since we changed the array to a number, the code "number = ary + 3" will work now.
end
But that still isn't it. Sometimes you need to make sure that some code is run under all circumstances. It could be difficult to repeat the code in all rescue blocks. And so there is another helpful keyword: ensure. It does exactly what it says. It ensures that whatever code is under it, will be executed, EVEN IF AN EXCEPTION WAS NOT HANDLED BY OUR RESCUE CODES! This is important to understand. Ensure will run, even if Ruby has to stop after an unhandled exception. This is why many Ruby programmers use begin end to open files. It is dangerous and even destructive to open files, but not closing them after use. But an unhandled exception will lead to exactly that. It can be avoided with ensure, like so.
- Code: Select all
f = File.open("somefile")
#the file is now open and ready to be used#
begin
#code that works with the file#
rescue
#code that handles errors#
ensure
f.close unless f.nil?
#if f is nil, the file is unavailable, else close f#
end
If now an unhandled exception is raised, the file will be closed, before Ruby stops. However, ensure will always be executed, even if there was no exception, or if all exception were handled. That means, you have to make sure, that the code under ensure will not be executed elsewhere in the begin end block! In this example the file should not be closed in the rescue block!
I still have more for you. Sometimes, instead of just defining which exceptions to catch, you may want to access the exception in the rescue block (especially useful with your own exception, see below). You do that by referencing a local variable.
- Code: Select all
ary = [] #creates an array class instance
begin
number = ary + 3
rescue => exc #works also with "rescue TypeError => exc" etc., but is redundant
watch "I caught this", exc.message
number = 0
end
Last but not least, as well as handling exceptions, you can also raise one! This is helpful for various reasons, for example to re-raise an exception that was already handled. Some higher layer code might need to be informed of the error, even if it was handled.
- Code: Select all
ary = [] #creates an array class instance
begin
number = ary + 3
rescue TypeError, NameError
number = 0
rescue ZeroDivisionError
number = 0
raise
end
raise without any argument in the rescue block will re-raise the same eexception, here a ZeroDivisionError.
- Code: Select all
a = 0
begin
number = 6 / a
rescue TypeError, NameError
number = 0
rescue ZeroDivisionError
number = 6
raise "You idiot once again tried to divide by zero! Will you ever learn?"
end
Here you raise a RuntimeError with the message you can read there. You can also create your own exceptions that you can then raise. Exceptions are classes, just like everything else in Ruby. Just make sure to inherit from StandardError
- Code: Select all
class IdiotError < StandardError
def initialize(msg="You will never learn! Divided by Zero again!")
super
end
end
a = 0
begin
b = 3 / a
rescue
raise IdiotError
end
You can overwrite the default message as well.
- Code: Select all
class IdiotError < StandardError
def initialize(msg="You will never learn! Divided by Zero again!")
super
end
end
a = 0
begin
b = 3 / a
rescue
raise IdiotError, "OK, I was a bit harsh. But please stop dividing by Zero!"
end
I probably forgot about something. But this will give you a start into exception handling. Have fun programming!