The other day I came across some code that was making HTTP POST requests to a 3rd-party API. The API used 3 types of HTTP response codes:
Here is the code:
def self.send_message(message) begin response = Net::HTTP.post_form(URI.parse(URL), :message => message) case response when Net::HTTPOK true # success response when Net::HTTPClientError, Net::HTTPInternalServerError false # non-success response end rescue Timeout::Error => error HoptoadNotifier.notify error false # non-success response end end
The error handling in the above code seems to be casting too wide a net because the code in the legs of the case
statement will never raise a Timeout::Error
(Timeout::Error
is one of several errors that can be raised from Net::HTTP.post_form
; the above code has been simplified, it actually rescued several other errors as well).
So I refactored it to:
def self.send_message(message) begin response = Net::HTTP.post_form(URI.parse(URL), :message => message) rescue Timeout::Error => error HoptoadNotifier.notify error false # non-success response end case response when Net::HTTPOK true # success response when Net::HTTPClientError, Net::HTTPInternalServerError false # non-success response end end
It’s now very clear from the structure of the code where the Timeout::Error
could be raised. However, if a Timeout::Error
occurs now, the case
statement will still be executed. A little research led me to the following from The Ruby Programming Language:
The
else
clause is an alternative to therescue
clauses; it is used if none of therescue
clauses are needed. That is, the code in anelse
clause is executed if the code in the body of thebegin
statement runs to completion without exceptions.The use of an
else
clause is not particularly common in Ruby, but they can be stylistically useful to emphasize the difference between normal completion of a block of code and exceptional completion of a block of code.
Incorporating this into our code, leads to the following:
def self.send_message(message) begin response = Net::HTTP.post_form(URI.parse(URL), :message => message) rescue Timeout::Error => error HoptoadNotifier.notify error false # non-success response else case response when Net::HTTPOK true # success response when Net::HTTPClientError, Net::HTTPInternalServerError false # non-success response end end end
Now we have clear error handling and our case
statement will not be executed when an error is raised.
To wrap up the refactoring, we can remove the begin
and end
blocks thanks to another feature of error handling in Ruby.
the body of a method definition is an implicit begin-end block
def self.send_message(message) response = Net::HTTP.post_form(URI.parse(URL), :message => message) rescue Timeout::Error => error HoptoadNotifier.notify error false # non-success response else case response when Net::HTTPOK true # success response when Net::HTTPClientError, Net::HTTPInternalServerError false # non-success response end end