NOTE:This blog had a good run, but is now in retirement.
If you enjoy the content here, please support Gregory's ongoing work on the Practicing Ruby journal.

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:

  • return is executed,
  • break is executed in a do block invoked,
  • an exception is triggered via raise,
  • throw is 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 else section. This might also lead to unnecessary retries if there is a retry in the rescue clause.
  • Placing the code after end generally has a similar effect as putting it in an else section but if there is an ensure section order of execution between the two code bits is reversed. This can make a serious difference if the else code uses a resource which is cleaned up in ensure.
  • When using the version of begin ... end in 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 rescue clause is removed.
  • A previously uncaught exception type is thrown from the main section (the change need not be in your piece of code).
  • A rescue clause is changed to raise an exception.
  • An else section is introduced and code might throw.
  • Code in else section 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.

  1. If you have cleanup code that must be executed under all circumstances, put it in ensure.
  2. Do not place return or break in ensure sections and try to avoid throwing exceptions from them.
  3. Place rescue clauses for more specific exceptions (sub classes) before those for less specific ones (super classes).
  4. rescue the most specific exception you can handle.
  5. Do not rescue exceptions 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.

blog comments powered by Disqus