Exception Handling: Designing Robust Software ihower@gmail.com 2014/9/27@RailsPacific
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
Agenda • Why should we care? (5min) • Exception handling in Ruby (10min) • Caveat and Guideline (15min) • Failure handling strategy (15min)
I’m standing on the two great books, thanks.
1. Why should we care?
Feature complete == Production ready?
All tests pass?
Happy Path!
Time Cost Quality Pick Two?
abstract software architecture to reduce development cost
Development cost != Operational cost
– Michael T. Nygard, Release It! “Don't avoid one-time development expenses at the cost of recurring operational expenses.”
My confession
2. Exception handling in Ruby
– 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!.”
– 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.”
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
raise == fail begin # do something fail 'An error has occured.' rescue => e puts 'I am rescued.' puts e.message puts e.backtrace.inspect end
– 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.”
“raise” method signature ! raise(exception_class_or_object, message, backtrace)
raise raise ! # is equivalent to: ! raise RuntimeError
raise(string) raise 'An error has occured.' ! # is equivalent to : ! raise RuntimeError, "'An error has occured.'
raise(exception, string) raise RuntimeError, 'An error has occured.' ! # is equivalent to : ! raise RuntimeError.new('An error has occured.')
backtrace (Array of string) raise RuntimeError, 'An error' ! # is equivalent to : ! raise RuntimeError, 'An error', caller(0)
global $! variable ! • $! • $ERROR_INFO • reset to nil if the exception is rescued
2. rescue rescue SomeError => e # ... end 2/5
rescue SomeError, SomeOtherError => e # ... end Multiple class or module
rescue SomeError => e # ... rescue SomeOtherError => e # ... end stacking rescue order matters
rescue # ... end ! # is equivalent to: ! rescue StandardError # ... end
Ruby Exception Hierarchy • Exception • NoMemoryError • LoadError • SyntaxError • StandardError -- default for rescue • ArgumentError • IOError • NoMethodError • ….
Avoid rescuing Exception class rescue Exception => e # ... end
rescue => error # ... end # is equivalent to: ! rescue StandardError => error # ... end
support * splatted active_support/core_ext/kernel/reporting.rb suppress(IOError, SystemCallError) do open("NONEXISTENT_FILE") end ! puts 'This code gets executed.'
! def suppress(*exception_classes) yield rescue *exception_classes # nothing to do end support * splatted (cont.) active_support/core_ext/kernel/reporting.rb
Like case, it’s support === begin raise "Timeout while reading from socket" rescue errors_with_message(/socket/) puts "Ignoring socket error" end
def errors_with_message(pattern) m = Module.new m.singleton_class.instance_eval do define_method(:===) do |e| pattern === e.message end end m end
arbitrary block predicate begin raise "Timeout while reading from socket" rescue errors_matching{|e| e.message =~ /socket/} puts "Ignoring socket error" end
def errors_matching(&block) m = Module.new m.singleton_class.instance_eval do define_method(:===, &block) end m end
– 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.”
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
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
5. else begin yield rescue puts "Only on error" else puts "Only on success" ensure puts "Always executed" end 5/5
Recap • raise • rescue • ensure • retry • else
3. Caveat and Guideline
1. Is the situation truly unexpected? 1/6
– 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.”
User Input error?
def create @user = User.new params[:user] @user.save! redirect_to user_path(@user) rescue ActiveRecord::RecordNotSaved flash[:notice] = 'Unable to create user' render :action => :new end This is bad
def create @user = User.new params[:user] if @user.save redirect_to user_path(@user) else flash[:notice] = 'Unable to create user' render :action => :new end end
Record not found?
begin user = User.find(params[:id) user.do_this rescue ActiveRecord::RecordNotFound # ??? end This is bad
Use Null object user = User.find_by_id(params[:id) || NullUser.new user.do_this
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
– 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.”
Spare Handler? begin # main implementation rescue # alternative solution end
begin user = User.find(params[:id) rescue ActiveRecord::RecordNotFound user = NullUser.new ensure user.do_this end This is bad
user = User.find_by_id(params[:id) || NullUser.new ! user.do_this
2. raise during raise begin raise "Error A" # this will be thrown rescue raise "Error B" end 2/6
Wrapped exception ! begin begin raise "Error A" rescue => error raise MyError, "Error B" end rescue => e puts "Current failure: #{e.inspect}" puts "Original failure: #{e.original.inspect}" end
Wrapped exception (cont.) class MyError < StandardError attr_reader :original def initialize(msg, original=$!) super(msg) @original = original; set_backtrace original.backtrace end end
Example: Rails uses the technique a lot • ActionDispatch::ExceptionWrapper • ActionControllerError::BadRequest, ParseError,SessionRestoreError • ActionView Template::Error
3. raise during ensure begin raise "Error A" ensure raise "Error B” puts "Never execute" end 3/6
begin file1.open file2.open raise "Error" ensure file1.close # if there's an error file2.close end
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
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
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
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)
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
5. Exception classification module MyLibrary class Error < StandardError end end 5/6
exception hierarchy example • StandardError • ActionControllerError • BadRequest • RoutingError • ActionController::UrlGenerationError • MethodNotAllowed • NotImplemented • …
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
6. Readable exceptional code begin try_something rescue begin try_something_else rescue # ... end end end 6/6
Extract it! def foo try_something rescue bar end ! def bar try_something_else # ... rescue # ... end
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
4. Failure handling strategy
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
2. Operational errors v.s. programmer errors https://www.joyent.com/developers/node/design/errors 2/12
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
Programmer errors like typo
We can handle operational errors • Restore and Cleanup Resource • Handle it directly • Propagate • Retry • Crash • Log it
But we can not handle programmer errors
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
Level 0: Undefined • Service: Failing implicitly or explicitly • State: Unknown or incorrect • Lifetime: Terminated or continued
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
Anti-pattern:
 Dummy handler (eating exceptions) begin #... rescue => e nil end
Level 2: State-recovery (weakly tolerant) • Service: Failing explicitly • State: Correct • Lifetime: Continued • How-achieved: • Backward recovery • Cleanup
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
Level 3: Behavior-recovery (strongly tolerant) • Service: Delivered • State: Correct • Lifetime: Continued • How-achieved: • retry, and/or • design diversity, data diversity, and functional diversity
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.
4. Use timeout for any external call begin Timeout::timeout(3) do #... end rescue Timeout::Error => e # ... end 4/12
5. retry with circuit breaker http://martinfowler.com/bliki/CircuitBreaker.html 5/12
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
! 7. Failure reporting • A central log server • Email • exception_notification gem • 3-party exception-reporting service • Airbrake, Sentry New Relic…etc 7/12
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
9. caller-supplied fallback strategy h.fetch(:optional){ "default value" } h.fetch(:required){ raise "Required not found" } ! arr.fetch(999){ "default value" } arr.detect( lambda{ "default value" }) { |x| x == "target" } 9/12
– Brian Kernighan and Rob Pike, The Practice of Programming “In most cases, the caller should determine how to handle an error, not the callee.”
def do_something(failure_policy=method(:raise)) #... rescue => e failure_policy.call(e) end ! do_something{ |e| puts e.message }
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
11. Error code v.s. exception • error code problems • it mixes normal code and exception handling • programmer can ignore error code easily 11/12
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
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
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
– 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
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
12. throw/catch flow def halt(*response) #.... throw :halt, response end def invoke res = catch(:halt) { yield } #... end 12/12
Recap ! • exception safety • operational errors v.s. programmer errors • robust levels
Recap (cont.) • use timeout • retry with circuit breaker pattern • bulkheads • failure reporting • exception collection • caller-supplied fallback strategy • avoid unexpected termination • error code • throw/catch
Thanks for your listening
QUESTIONS! begin thanks! raise if question? rescue puts "Please ask @ihower at twitter" ensure follow(@ihower).get_slides applause! end
Reference • 例外處理設計的逆襲 • Exceptional Ruby http://avdi.org/talks/exceptional-ruby-2011-02-04/ • Release it! • Programming Ruby • The Well-Grounded Rubyist, 2nd • Refactoring • The Pragmatic Programmer • Code Complete 2 • https://www.joyent.com/developers/node/design/errors • http://robots.thoughtbot.com/save-bang-your-head-active-record-will-drive-you-mad • http://code.tutsplus.com/articles/writing-robust-web-applications-the-lost-art-of-exception-handling--net-36395 • http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx

Exception Handling: Designing Robust Software in Ruby

  • 1.
    Exception Handling: Designing RobustSoftware ihower@gmail.com 2014/9/27@RailsPacific
  • 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 shouldwe care? (5min) • Exception handling in Ruby (10min) • Caveat and Guideline (15min) • Failure handling strategy (15min)
  • 4.
    I’m standing onthe two great books, thanks.
  • 5.
    1. Why shouldwe care?
  • 6.
  • 7.
  • 8.
  • 10.
  • 11.
    abstract software architecture toreduce development cost
  • 12.
  • 13.
    – Michael T.Nygard, Release It! “Don't avoid one-time development expenses at the cost of recurring operational expenses.”
  • 14.
  • 15.
  • 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.”
  • 21.
  • 22.
    raise raise ! # is equivalentto: ! raise RuntimeError
  • 23.
    raise(string) raise 'An errorhas occured.' ! # is equivalent to : ! raise RuntimeError, "'An error has occured.'
  • 24.
    raise(exception, string) raise RuntimeError,'An error has occured.' ! # is equivalent to : ! raise RuntimeError.new('An error has occured.')
  • 25.
    backtrace (Array of string) raiseRuntimeError, 'An error' ! # is equivalent to : ! raise RuntimeError, 'An error', caller(0)
  • 26.
    global $! variable ! •$! • $ERROR_INFO • reset to nil if the exception is rescued
  • 27.
    2. rescue rescue SomeError=> e # ... end 2/5
  • 28.
    rescue SomeError, SomeOtherError=> e # ... end Multiple class or module
  • 29.
    rescue SomeError =>e # ... rescue SomeOtherError => e # ... end stacking rescue order matters
  • 30.
    rescue # ... end ! # isequivalent to: ! rescue StandardError # ... end
  • 31.
    Ruby Exception Hierarchy •Exception • NoMemoryError • LoadError • SyntaxError • StandardError -- default for rescue • ArgumentError • IOError • NoMethodError • ….
  • 32.
    Avoid rescuing Exception class rescueException => e # ... end
  • 33.
    rescue => error #... end # is equivalent to: ! rescue StandardError => error # ... end
  • 34.
    support * splatted active_support/core_ext/kernel/reporting.rb suppress(IOError,SystemCallError) do open("NONEXISTENT_FILE") end ! puts 'This code gets executed.'
  • 35.
    ! def suppress(*exception_classes) yield rescue *exception_classes #nothing to do end support * splatted (cont.) active_support/core_ext/kernel/reporting.rb
  • 36.
    Like case, it’ssupport === begin raise "Timeout while reading from socket" rescue errors_with_message(/socket/) puts "Ignoring socket error" end
  • 37.
    def errors_with_message(pattern) m =Module.new m.singleton_class.instance_eval do define_method(:===) do |e| pattern === e.message end end m end
  • 38.
    arbitrary block predicate begin raise"Timeout while reading from socket" rescue errors_matching{|e| e.message =~ /socket/} puts "Ignoring socket error" end
  • 39.
    def errors_matching(&block) m =Module.new m.singleton_class.instance_eval do define_method(:===, &block) end m end
  • 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 # dosomething 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
  • 43.
    5. else begin yield rescue puts "Onlyon error" else puts "Only on success" ensure puts "Always executed" end 5/5
  • 44.
    Recap • raise • rescue •ensure • retry • else
  • 45.
    3. Caveat andGuideline
  • 46.
    1. Is thesituation truly unexpected? 1/6
  • 47.
    – Dave Thomasand 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.”
  • 48.
  • 49.
    def create @user =User.new params[:user] @user.save! redirect_to user_path(@user) rescue ActiveRecord::RecordNotSaved flash[:notice] = 'Unable to create user' render :action => :new end This is bad
  • 50.
    def create @user =User.new params[:user] if @user.save redirect_to user_path(@user) else flash[:notice] = 'Unable to create user' render :action => :new end end
  • 51.
  • 52.
    begin user = User.find(params[:id) user.do_this rescueActiveRecord::RecordNotFound # ??? end This is bad
  • 53.
    Use Null object user= User.find_by_id(params[:id) || NullUser.new user.do_this
  • 54.
    Replace Exception withTest 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.”
  • 56.
    Spare Handler? begin # mainimplementation rescue # alternative solution end
  • 57.
    begin user = User.find(params[:id) rescueActiveRecord::RecordNotFound user = NullUser.new ensure user.do_this end This is bad
  • 58.
    user = User.find_by_id(params[:id)|| NullUser.new ! user.do_this
  • 59.
    2. raise duringraise begin raise "Error A" # this will be thrown rescue raise "Error B" end 2/6
  • 60.
    Wrapped exception ! begin begin raise "ErrorA" rescue => error raise MyError, "Error B" end rescue => e puts "Current failure: #{e.inspect}" puts "Original failure: #{e.original.inspect}" end
  • 61.
    Wrapped exception (cont.) classMyError < StandardError attr_reader :original def initialize(msg, original=$!) super(msg) @original = original; set_backtrace original.backtrace end end
  • 62.
    Example: Rails usesthe technique a lot • ActionDispatch::ExceptionWrapper • ActionControllerError::BadRequest, ParseError,SessionRestoreError • ActionView Template::Error
  • 63.
    3. raise duringensure begin raise "Error A" ensure raise "Error B” puts "Never execute" end 3/6
  • 64.
  • 65.
    a more complexexample 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 ! definitialize(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 r1exception and fail to close r2 run 1 run 2 close 1 double_raise.rb:15:in `close': close error: 1 (RuntimeError)
  • 69.
    4. Exception isyour 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
  • 70.
    5. Exception classification moduleMyLibrary class Error < StandardError end end 5/6
  • 71.
    exception hierarchy example • StandardError •ActionControllerError • BadRequest • RoutingError • ActionController::UrlGenerationError • MethodNotAllowed • NotImplemented • …
  • 72.
    smart exception adding moreinformation 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
  • 73.
  • 74.
    Extract it! def foo try_something rescue bar end ! defbar try_something_else # ... rescue # ... end
  • 75.
    Recap • Use exceptionwhen you need • Wrap exception when re-raising • Avoid raising during ensure • Exception is your method interface too • Classify your exceptions • Readable exceptional code
  • 76.
  • 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
  • 78.
    2. Operational errors v.s. programmererrors https://www.joyent.com/developers/node/design/errors 2/12
  • 79.
    Operational errors • failedto connect to server • failed to resolve hostname • request timeout • server returned a 500 response • socket hang-up • system is out of memory
  • 80.
  • 81.
    We can handleoperational errors • Restore and Cleanup Resource • Handle it directly • Propagate • Retry • Crash • Log it
  • 82.
    But we cannot handle programmer errors
  • 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 (Failingfast) • 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
  • 86.
    Anti-pattern:
 Dummy handler (eatingexceptions) begin #... rescue => e nil end
  • 87.
    Level 2: State-recovery (weaklytolerant) • Service: Failing explicitly • State: Correct • Lifetime: Continued • How-achieved: • Backward recovery • Cleanup
  • 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 (stronglytolerant) • 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 forany external call begin Timeout::timeout(3) do #... end rescue Timeout::Error => e # ... end 4/12
  • 92.
    5. retry withcircuit breaker http://martinfowler.com/bliki/CircuitBreaker.html 5/12
  • 93.
    6. bulkheads for externalservice 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 classItem def process #... [true, result] rescue => e [false, result] end end ! collections.each do |item| results << item.process end 8/12
  • 96.
    9. caller-supplied fallback strategy h.fetch(:optional){"default value" } h.fetch(:required){ raise "Required not found" } ! arr.fetch(999){ "default value" } arr.detect( lambda{ "default value" }) { |x| x == "target" } 9/12
  • 97.
    – Brian Kernighanand Rob Pike, The Practice of Programming “In most cases, the caller should determine how to handle an error, not the callee.”
  • 98.
    def do_something(failure_policy=method(:raise)) #... rescue =>e failure_policy.call(e) end ! do_something{ |e| puts e.message }
  • 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 codev.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 Codewith 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 Gonot 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'sreally 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 errorcode? ! • 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 defhalt(*response) #.... throw :halt, response end def invoke res = catch(:halt) { yield } #... end 12/12
  • 107.
    Recap ! • exception safety •operational errors v.s. programmer errors • robust levels
  • 108.
    Recap (cont.) • usetimeout • retry with circuit breaker pattern • bulkheads • failure reporting • exception collection • caller-supplied fallback strategy • avoid unexpected termination • error code • throw/catch
  • 109.
  • 110.
    QUESTIONS! begin thanks! raise if question? rescue puts"Please ask @ihower at twitter" ensure follow(@ihower).get_slides applause! end
  • 111.
    Reference • 例外處理設計的逆襲 • ExceptionalRuby http://avdi.org/talks/exceptional-ruby-2011-02-04/ • Release it! • Programming Ruby • The Well-Grounded Rubyist, 2nd • Refactoring • The Pragmatic Programmer • Code Complete 2 • https://www.joyent.com/developers/node/design/errors • http://robots.thoughtbot.com/save-bang-your-head-active-record-will-drive-you-mad • http://code.tutsplus.com/articles/writing-robust-web-applications-the-lost-art-of-exception-handling--net-36395 • http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx