Ruby Exceptions
Exceptions and execution are always linked together. If you try to open a non-existent file and do not handle this situation properly, your program is considered to be of low quality.
If an exception occurs, the program stops. Exceptions are used to handle various types of errors that may occur during program execution, so appropriate actions can be taken without causing the program to completely halt.
Ruby provides a perfect mechanism for handling exceptions. We can attach code that may throw exceptions within a begin/end block and use the rescue clause to tell Ruby the types of exceptions we want to handle.
Syntax
begin # Start
raise.. # Throw exception
rescue [ExceptionType = StandardException] # Catch specified type of exception, default is StandardException
$! # Represents exception information
$@ # Represents the code location where the exception occurred
else # Other exceptions
..
ensure # Regardless of whether an exception occurs, enter this code block
end # End
Everything from begin to rescue is protected. If an exception occurs during the execution of the code block, control is passed to the block between rescue and end.
For each rescue clause in the begin block, Ruby compares the thrown exception with each parameter in turn. If the exception named in the rescue clause is the same as the current thrown exception type or a parent class of it, the match is successful.
If the exception does not match any specified error types, we can use an else clause after all the rescue clauses.
Example
#!/usr/bin/ruby
begin
file = open("/unexistant_file")
if file
puts "File opened successfully"
end
rescue
file = STDIN
end
print file, "==", STDIN, "\n"
The output result of the above example is as follows. You can see that STDIN replaces file because the open operation failed.
#<IO:0xb7d16f84>==#<IO:0xb7d16f84>
Using the retry Statement
You can use the rescue block to catch exceptions and then use the retry statement to restart execution from the beginning of the begin block.
Syntax
begin
# Exceptions thrown by this code will be caught by the following rescue clause
rescue
# This block will catch all types of exceptions
retry # This will move control back to the beginning of begin
end
Example
#!/usr/bin/ruby
begin
file = open("/unexistant_file")
if file
puts "File opened successfully"
end
rescue
fname = "existant_file"
retry
end
The following is the processing flow:
- An exception occurs when opening.
- Jump to rescue. fname is reassigned.
- Jump to the beginning of begin via retry.
- The file opens successfully this time.
- Continue with the basic process.
Note: If the renamed file does not exist, this example code will attempt indefinitely. Therefore, use retry cautiously when handling exceptions.
Using the raise Statement
You can use the raise statement to throw exceptions. The following method throws an exception when called. Its second message will be output.
Syntax
raise
or
raise "Error Message"
or
raise ExceptionType, "Error Message"
or
raise ExceptionType, "Error Message" condition
The first form simply rethrows the current exception (or a RuntimeError if there is no current exception). This is used in exception handlers that need to interpret the exception before passing it on.
The second form creates a new RuntimeError exception, setting its message to the given string. This exception is then thrown up the call stack.
The third form creates an exception using the first parameter and sets the related message to the second parameter.
The fourth form is similar to the third form, and you can add any additional conditional statements (such as unless) to throw the exception.
Example
#!/usr/bin/ruby
begin
puts 'I am before the raise.'
raise 'An error has occurred.'
puts 'I am after the raise.'
rescue
puts 'I am rescued.'
end
puts 'I am after the begin block.'
The output result of the above example is:
I am before the raise.
I am rescued.
I am after the begin block.
Another example demonstrating the use of raise:
Example
#!/usr/bin/ruby
begin
raise 'A test exception.'
rescue Exception => e
puts e.message
puts e.backtrace.inspect
end
The output result of the above example is:
A test exception.
["main.rb:4"]
Using the ensure Statement
Sometimes, regardless of whether an exception is thrown, you need to ensure that some processing is completed at the end of the code block. For example, you might open a file when entering and need to ensure it is closed when exiting the block.
The ensure clause does this. Ensure is placed after the last rescue clause and contains a block of code that always executes when the block terminates. It doesn't matter whether the block exits normally, throws and handles an exception, or terminates due to an uncaught exception; the ensure block always runs.
Syntax
begin
#.. process
#.. throw exception
rescue
#.. handle error
ensure
#.. final ensure execution
#.. this always executes
end
Example
begin
raise 'A test exception.'
rescue Exception => e
puts e.message
puts e.backtrace.inspect
ensure
puts "Ensuring execution"
end
The output result of the above example is:
A test exception.
["main.rb:4"]
Ensuring execution
Using the else Statement
If an else clause is provided, it is usually placed after the rescue clause and before any ensure.
The body of the else clause executes only if the code body does not throw an exception.
Syntax
begin
#.. process
#.. throw exception
rescue
#.. handle error
else
#.. execute if no exceptions
ensure
#.. final ensure execution
#.. this always executes
end
Example
begin
# Throw 'A test exception.'
puts "I'm not raising exception"
rescue Exception => e
puts e.message
puts e.backtrace.inspect
else
puts "Congratulations-- no errors!"
ensure
puts "Ensuring execution"
end
The output result of the above example is:
I'm not raising exception
Congratulations-- no errors!
Ensuring execution
The $! variable can be used to catch the thrown error message.
Catch and Throw
The raise and rescue exception mechanism aborts execution when an error occurs. Sometimes, you need to jump out of some deeply nested structures during normal processing. This is where catch and throw come in handy.
catch defines a block that uses a given name (either a Symbol or String) as a label. The block executes normally until a throw is encountered.
Syntax
throw :lablename
#.. this will not be executed
catch :lablename do
#.. catch will be executed after a throw is encountered
end
or
throw :lablename condition
#.. this will not be executed
catch :lablename do
#.. catch will be executed after a throw is encountered
end
Example
In the following example, if the user types '!' in response to any prompt, a throw terminates the interaction with the user.
Example
def promptAndGet(prompt)
print prompt
res = readline.chomp
throw :quitRequested if res == "!"
return res
end
catch :quitRequested do
name = promptAndGet("Name: ")
age = promptAndGet("Age: ")
sex = promptAndGet("Sex: ")
# ..
# process information
end
promptAndGet("Name:")
The above program requires manual interaction, which you can try on your computer. The output result of the above example is:
Name: Ruby on Rails
Age: 3
Sex: !
Name:Just Ruby
Class Exception
Ruby's standard classes and modules throw exceptions. All exception classes form a hierarchy, including the top-level Exception class. The next level comprises seven different types:
- Interrupt
- NoMemoryError
- SignalException
- ScriptError
- StandardError
- SystemExit
Fatal is another exception at this level, but the Ruby interpreter only uses it internally.
ScriptError and StandardError both have some subclasses, but we don't need to know the details here. The most important thing is to create our own exception classes, which must be subclasses of Exception or its descendants.
Let's look at an example:
Example
class FileSaveError < StandardError
attr_reader :reason
def initialize(reason)
@reason = reason
end
end
Now, look at the following example, which will use the above exception:
Example
File.open(path, "w") do |file|
begin
# Write data ...
rescue
# Error occurred
raise FileSaveError.new($!)
end
end
Here, the most important line is raise FileSaveError.new($!). We call raise to signal that an exception has occurred, passing it a new instance of FileSaveError, caused by a specific exception leading to a data write failure.