Back to Home

 Catch and Throw and Rescue

Sep 24, 2017 

I am currently reading Avdi Grimm's Confident Ruby and it has been great learning curve. I also discoverered Ruby Book Club podcast by Nadia and Saron and it has been a great journey to read a chapter by chapter and listening ladies discuss about the book.

Anyway, I found this chapter - 5.7 Early Termination with Throw, very interesting so would like to introduce to ya all.

So When to use Catch and Throw

First question I need to ask before using catch and throw is that when to use. This is most likely useful when we want to stop the recursive process without throwing an error.

Use throw to signal the early termination, and catch to handle it.

floor = [["blank", "blank", "blank"], ["gummy", "blank", "blank"], ["blank", "blank", "blank"]] catch(:found) do floor.each do |line| line.each do |word| counter += 1 if word == "gummy" candy = word throw :found end end end end

Catch and Throw could be used more complex cases but in nutshell, it allows the program stops when it found essential value. This pattern also limit outselves using raising Errors to stop the program e.g. raise DoneError in the middle of the method or using begine, rescue end (BRE) block

Avdi Grimm also explains why we should not use BRE block - he says "BRE is the pink lawn Xamingo of Ruby code: it is a distraction and an eyesore wherever it turns up" because it breaks flows of the method.

def some_method # do something begin # do more thing rescue # handle error if raises end # do some more work end

His suggestion for the error handling case is using top-level rescue clause.

def foo # happy path goes here rescue # failure ended up here end

This will clearly shows what foo function supposed to be. If things goes south, it can be handled by rescue. Let's look at another trick called 'Bouncer method'. A Bouncer Method is a method whose sole job is to raise an exception if it detects an error condition.

# original method def send_user_welcome_email(list_of_user_ids) list_of_user_ids.each do |user_id| user = User.find(user_id) send_emai_to_user(user_id) user.welcome_email_sent = true end unless user.save? raise ArgumentError, "user is not valid" end Rails.logger.info "welcome email is sent to #{user}.fullname" end

Enter the bouncer method.

# bouncer method def check_valid_status(user) unless user.save? raise ArgumentError, "user is not valid" end end # now it can be: def send_user_welcome_email(list_of_user_ids) list_of_user_ids.each do |user_id| user = User.find(user_id) send_emai_to_user(user_id) user.welcome_email_sent = true end check_valid_status(user) Rails.logger.info "welcome email is sent to #{user}.fullname" end

We can even go further. We can yield the result of the main method and then raise error if the result is failed.

def check_valid_status result = yield unless result.save? raise ArgumentError, "user is not valid" end end def send_user_welcome_email(list_of_user_ids) check_valid_status do list_of_user_ids.each do |user_id| user = User.find(user_id) send_emai_to_user(user_id) user.welcome_email_sent = true end end end

Conclusion: Use top-level rescue to rescue failure. Think of flow of the method. "When given an intention-revealing name, it can clearly and concisely reveal to the reader exactly what potential failure is being detected. And it can DRY up identical error-checking code in other parts of the program."