The Universe between begin and end
2009-05-01 10:05, written by Robert Klemme
This time we’ll explore the space between begin and end. Today’s article won’t be as much about individual best practices but rather I will try to explore various aspects of begin ... end blocks so you can decide how to make best use of this tool. In fact, there are so many aspects that this construct sometimes reminds me of a swiss army knife – not so much because you can do everything with it but rather because it has so many features that you can use. When it comes to control structuring elements begin ... end is probably the most complex thing found in Ruby.
One final introductory remark: although I am pretty sure that Ruby hasn’t changed between 1.8 and 1.9 with regard to begin ... end I did my tests with 1.9.1 only. So if you find something to be wrong, please comment!
Starting the Investigation
For easier reference here’s a block which contains all the options:
begin # do our work rescue # standard oops! rescue SomeException => e # oops! rescue Exception => e # deal with other errors else # good, no exception surfaced! ensure # good or bad, this needs to be done end
Note that you can replace begin with def meth... to define a method with “integrated” exception handling. That way you can avoid one level of nesting.
There are various interesting aspects to each section:
- control flow (most particularly, how is the section left?),
- result (if any),
- documentation (what does it tell me that code is in a particular section?).
We’ll keep these in the back of our heads when exploring section after section.
begin
This is the main section where you place the code that does the work you want to get done. If evaluation reaches the end of this section normally the result of the last expression evaluated is also the result of this section. In the absence of rescue or else that value is propagated to the surrounding context.
If we look at control flow things start to get a bit more involved. First, you should note that there are these additional ways this section (actually any section of code) can be terminated:
returnis executed,breakis executed in adoblock invoked,- an exception is triggered via
raise, throwis invoked.
How many of those did you think of? (Did I miss another one?) All these have one thing in common: the end of the section is not reached normally and the code produces a different result.
rescue
This can have an optional ExceptionType => variable in which case exceptions of this class and subclasses (if there are no preceeding rescue clauses with them) are caught. If that optional part is missing only StandardError and subclasses are caught.
If there are multiple rescue clauses, order matters. You must rescue most specific errors first and less specificerrors later because otherwise a super class rescue clause will shadow a sub class clause which comes later. Only one rescue clause of a group is ever executed.
In case a rescue clause is executed the result of the whole block is that of the code in the rescue clause. In other words: when catching exceptions the exception code replaces the block’s result. Unless, that is, you invoke raise or retry.
The tricky part about rescue is which exceptions to catch. If you cannot handle an exception, you should not catch it because otherwise you will prevent other code that is capable of handling it to work. Actually, as long as you do not raise again, nobody outside will notice that there was an error in the first place. A special case is raise without arguments: sometimes it is reasonable to catch all exceptions, log the event and rethrow:
def f
do_work
rescue Exception => e
log.error "There was an error: #{e.message}"
raise
end
else
This section does make sense only if there is a rescue as well. (You’ll get a warning if you use it without it.) If the main section completes regularly and an else section is present it is executed.
Now, some of the discussion revolved around the utility of this section and whether code placed here is equivalent to code placed in other places. While at first sight it may seem that you can just place it at the end of the main section a closer look reveals some subtleties, some of which have been mentioned already in the discussion. I’ll list them here anyway for completeness reasons:
- If the code raises an exception, it might be rescued when placed in the main section but it won’t be rescued when placed in
elsesection. This might also lead to unnecessary retries if there is aretryin therescueclause. - Placing the code after
endgenerally has a similar effect as putting it in anelsesection but if there is anensuresection order of execution between the two code bits is reversed. This can make a serious difference if theelsecode uses a resource which is cleaned up inensure. - When using the version of
begin ... endin a method definition there is no place “after end” which is part of the method invocation:
def do_the_work some_code_which_may_throw rescue ArgumentError $stderr.puts "Ooops! Passed the wrong argument." else puts "Job done." end
ensure
This section is the last one before the final end. Code in this section will be executed regardless of how the main section is left, i.e. even in case of raise, throw, return and break! The result of the section is ignored unless you choose to explicitly return it or raise an exception. While raising exceptions might be reasonable in some cases, it should generally be avoided because those exceptions will shadow errors coming from the main section or from rescue clauses:
irb(main):007:0> def f
irb(main):008:1> raise "Foo"
irb(main):009:1> ensure
irb(main):010:1* raise "Bar"
irb(main):011:1> end
=> nil
irb(main):012:0> f
RuntimeError: Bar
from (irb):10:in `ensure in f'
from (irb):10:in `f'
from (irb):12
from /usr/local/bin/irb19:12:in `<main>'
irb(main):013:0>
Another thing you should definitively avoid is a return statement in ensure because this will shadow the result from the “business logic” code in the main section:
irb(main):013:0> def f irb(main):014:1> 1 irb(main):015:1> ensure irb(main):016:1* 2 irb(main):017:1> end => nil irb(main):018:0> f => 1 irb(main):019:0> def g irb(main):020:1> 1 irb(main):021:1> ensure irb(main):022:1* return 2 irb(main):023:1> end => nil irb(main):024:0> g => 2 irb(main):025:0>
There’s a reason why the result of executing ensure section is ignored: this code has nothing to do with the “business logic” which should go into the main section but is solely for cleaning up. Assume you open a file with io = File.open (which I hope you won’t do after reading a previous post), have search code in the main section and place io.close in an ensure section. Then you would not want to shadow the result of the search by the result from the close operation.
Although ehsanul questioned the utility of ensure I certainly use it more often than else. While else section code can be put somewhere else in some cases, there is no other place of code which can easily replace an ensure clause and guarantee the same robustness of the code at the same time. This is nicely demonstrated by his suggested alternative which uses only rescue without Exception — this does not catch all exceptions! “Robustness” in this case refers not only to runtime robustness but also robustness of the code against maintenance (i.e. changes).
Let’s assume you have caught all exceptions via rescue clauses which do not raise and placed your cleanup code after end as it was suggested. Code works as expected and everything is fine. You might have to use a local variable to make sure the proper value is returned from your method but this is just a nuisance. Now all these changes will put execution of your cleanup code at danger:
- A
rescueclause is removed. - A previously uncaught exception type is thrown from the main section (the change need not be in your piece of code).
- A
rescueclause is changed to raise an exception. - An
elsesection is introduced and code might throw. - Code in
elsesection is changed to raise an exception.
And lastly, do not underestimate readability! Placing code in an ensure section tells the reader immediately that he can ignore it when trying to understand what the main purpose of the code is. This is “only” cleanup which makes sure some resources that were used for the calculation are not kept longer than needed. Whereas, if you place that code after end it could belong to the normal flow of the core business logic.
Guidelines
So after all the detail here are some rules which I hope will guide you in making the most appropriate use of begin ... end blocks.
- If you have cleanup code that must be executed under all circumstances, put it in
ensure. - Do not place
returnorbreakinensuresections and try to avoid throwing exceptions from them. - Place
rescueclauses for more specific exceptions (sub classes) before those for less specific ones (super classes). rescuethe most specific exception you can handle.- Do not
rescueexceptions that you cannot or do not want to handle.
Keep in mind that these are just guidelines and you have to check on a case by case basis which is the most appropriate solution. As Buddha says: verify the teaching with your own mind.