2. About me
• 張⽂文鈿 a.k.a. ihower
• http://ihower.tw
• http://twitter.com/ihower
• Instructor at ALPHA Camp
• http://alphacamp.tw
• Rails Developer since 2006
• i.e. Rails 1.1.6 era
3. Agenda
• Why should we care? (5min)
• Exception handling in Ruby (10min)
• Caveat and Guideline (15min)
• Failure handling strategy (15min)
16. – Steve McConnell, Code Complete
“If code in one routine encounters an
unexpected condition that it doesn’t know how
to handle, it throws an exception, essentially
throwing up its hands and yelling “I don’t know
what to do about this — I sure hope somebody
else knows how to handle it!.”
17. – Devid A. Black, The Well-Grounded Rubyist
“Raising an exception means stopping normal
execution of the program and either dealing with
the problem that’s been encountered or exiting
the program completely.”
18. 1. raise exception
begin
# do something
raise 'An error has occured.'
rescue => e
puts 'I am rescued.'
puts e.message
puts e.backtrace.inspect
end
1/5
19. raise == fail
begin
# do something
fail 'An error has occured.'
rescue => e
puts 'I am rescued.'
puts e.message
puts e.backtrace.inspect
end
20. – Jim Weirich, Rake author
“I almost always use the "fail" keyword. . .
[T]he only time I use “raise” is when I am
catching an exception and re-raising it,
because here I’m not failing, but explicitly
and purposefully raising an exception.”
40. – Bertrand Meyer, Object Oriented Software Construction
“In practice, the rescue clause should be a
short sequence of simple instructions designed
to bring the object back to a stable state and to
either retry the operation or terminate with
failure.”
41. 3. ensure
begin
# do something
raise 'An error has occured.'
rescue => e
puts 'I am rescued.'
ensure
puts 'This code gets executed always.'
end
3/5
42. 4. retry
be careful “giving up” condition
tries = 0
begin
tries += 1
puts "Trying #{tries}..."
raise "Didn't work"
rescue
retry if tries < 3
puts "I give up"
end
4/5
47. – Dave Thomas and Andy Hunt, The Pragmatic Programmer
“ask yourself, 'Will this code still run if I
remove all the exception handlers?" If the
answer is "no", then maybe exceptions are
being used in non-exceptional
circumstances.”
54. Replace Exception with Test
def execute(command)
command.prepare rescue nil
command.execute
end
!
# =>
!
def execute(command)
command.prepare if command.respond_to? :prepare
command.execute
end
55. – Martin Folwer, Refactoring
“Exceptions should be used for exceptional
behaviour. They should not acts as substitute
for conditional tests. If you can reasonably
expect the caller to check the condition before
calling the operation, you should provide a test,
and the caller should use it.”
65. a more complex example
begin
r1 = Resource.new(1)
r2 = Resource.new(2)
r2.run
r1.run
rescue => e
raise "run error: #{e.message}"
ensure
r2.close
r1.close
end
66. class Resource
attr_accessor :id
!
def initialize(id)
self.id = id
end
!
def run
puts "run #{self.id}"
raise "run error: #{self.id}"
end
!
def close
puts "close #{self.id}"
raise "close error: #{self.id}"
end
end
67. begin
r1 = Resource.new(1)
r2 = Resource.new(2)
r2.run
r1.run # raise exception!!!
rescue => e
raise "run error: #{e.message}"
ensure
r2.close # raise exception!!!
r1.close # never execute!
end
68. Result
lost original r1 exception and fail to close r2
run 1
run 2
close 1
double_raise.rb:15:in `close': close error: 1 (RuntimeError)
69. 4. Exception is your method
interface too
• For library, either you document your exception, or
you should consider no-raise library API
• return values (error code)
• provide fallback callback
4/6
72. smart exception
adding more information
class MyError < StandardError
attr_accessor :code
!
def initialize(code)
self.code = code
end
end
!
begin
raise MyError.new(1)
rescue => e
puts "Error code: #{e.code}"
end
75. Recap
• Use exception when you need
• Wrap exception when re-raising
• Avoid raising during ensure
• Exception is your method interface too
• Classify your exceptions
• Readable exceptional code
77. 1. Exception Safety
• no guarantee
• The weak guarantee (no-leak): If an exception is raised,
the object will be left in a consistent state.
• The strong guarantee (a.k.a. commit-or-rollback, all-or-
nothing): If an exception is raised, the object will be rolled
back to its beginning state.
• The nothrow guarantee (failure transparency): No
exceptions will be raised from the method. If an exception
is raised during the execution of the method it will be
handled internally.
1/12
79. Operational errors
• failed to connect to server
• failed to resolve hostname
• request timeout
• server returned a 500 response
• socket hang-up
• system is out of memory
83. 3. Robust levels
• Robustness: the ability of a system to resist change
without adapting its initial stable configuration.
• There’re four robust levels
http://en.wikipedia.org/wiki/Robustness 3/12
84. Level 0: Undefined
• Service: Failing implicitly or explicitly
• State: Unknown or incorrect
• Lifetime: Terminated or continued
85. Level 1: Error-reporting
(Failing fast)
• Service: Failing explicitly
• State: Unknown or incorrect
• Lifetime: Terminated or continued
• How-achieved:
• Propagating all unhandled exceptions, and
• Catching and reporting them in the main program
88. require 'open-uri'
page = "titles"
file_name = "#{page}.html"
web_page = open("https://pragprog.com/#{page}")
output = File.open(file_name, "w")
begin
while line = web_page.gets
output.puts line
end
output.close
rescue => e
STDERR.puts "Failed to download #{page}"
output.close
File.delete(file_name)
raise
end
89. Level 3: Behavior-recovery
(strongly tolerant)
• Service: Delivered
• State: Correct
• Lifetime: Continued
• How-achieved:
• retry, and/or
• design diversity, data diversity, and functional
diversity
90. Improve exception handling
incrementally
• Level 0 is bad
• it’s better we require all method has Level 1
• Level 2 for critical operation. e.g storage/database
operation
• Level 3 for critical feature or customer requires. it
means cost * 2 because we must have two solution
everywhere.
91. 4. Use timeout
for any external call
begin
Timeout::timeout(3) do
#...
end
rescue Timeout::Error => e
# ...
end
4/12
92. 5. retry with circuit breaker
http://martinfowler.com/bliki/CircuitBreaker.html 5/12
93. 6. bulkheads
for external service and process
begin
SomeExternalService.do_request
rescue Exception => e
logger.error "Error from External Service"
logger.error e.message
logger.error e.backtrace.join("n")
end
6/12
94. !
7. Failure reporting
• A central log server
• Email
• exception_notification gem
• 3-party exception-reporting service
• Airbrake, Sentry New Relic…etc
7/12
95. 8. Exception collection
class Item
def process
#...
[true, result]
rescue => e
[false, result]
end
end
!
collections.each do |item|
results << item.process
end
8/12
99. 10. Avoid unexpected
termination
• rescue all exceptions at the outermost call stacks.
• Rails does a great job here
• developer will see all exceptions at development
mode.
• end user will not see exceptions at production
mode
10/12
100. 11. Error code v.s. exception
• error code problems
• it mixes normal code and exception handling
• programmer can ignore error code easily
11/12
101. Error code problem
prepare_something # return error code
trigger_someaction # still run
http://yosefk.com/blog/error-codes-vs-exceptions-critical-code-vs-typical-code.html
102. Replace Error Code with
Exception (from Refactoring)
def withdraw(amount)
return -1 if amount > @balance
@balance -= amount
0
end
!
# =>
!
def withdraw(amount)
raise BalanceError.new if amount > @balance
@balance -= amount
end
103. Why does Go not have
exceptions?
• We believe that coupling exceptions to a control
structure, as in the try-catch-finally idiom, results in
convoluted code. It also tends to encourage
programmers to label too many ordinary errors, such as
failing to open a file, as exceptional. Go takes a different
approach. For plain error handling, Go's multi-value
returns make it easy to report an error without
overloading the return value. A canonical error type,
coupled with Go's other features, makes error handling
pleasant but quite different from that in other languages.
https://golang.org/doc/faq#exceptions
104. – Raymond Chen
“It's really hard to write good exception-based
code since you have to check every single line
of code (indeed, every sub-expression) and
think about what exceptions it might raise and
how your code will react to it.”
http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx
105. When use error code?
!
• In low-level code which you must know the
behavior in every possible situation
• error codes may be better
• Otherwise we have to know every exception that
can be thrown by every line in the method to
know what it will do
http://stackoverflow.com/questions/253314/exceptions-or-error-codes
106. 12. throw/catch flow
def halt(*response)
#....
throw :halt, response
end
def invoke
res = catch(:halt) { yield }
#...
end
12/12