Summarized using AI

Exceptional Ruby

Avdi Grimm • September 29, 2011 • New Orleans, LA • Talk

Exceptional Ruby: Understanding Error Handling in Ruby

In the talk "Exceptional Ruby" by Avdi Grimm at RubyConf 2011, the focus is on the intricacies of Ruby's exception handling system. He discusses how exceptions work in Ruby and how developers can leverage them to create robust error handling strategies in applications.

Key Points Discussed:

  • Definition of Failure: The speaker describes failure in programming through the lens of design by contract, emphasizing that every method has a contract with its caller. A failure occurs when a method cannot fulfill this contract for various reasons including invalid arguments, coding mistakes, or external system failures.

  • Exception Lifecycle in Ruby:

    • The talk starts with the mechanics of raising exceptions, beginning with the raise method and its various forms: raising a runtime error, custom classes, and modifying the backtrace.
    • Emphasizes that raise is a method and can be redefined, providing examples of custom implementations that enhance error handling capabilities (e.g. interactive error consoles).
  • Handling Exceptions:

    • Details on how Ruby manages exceptions using rescue, explaining its syntax and how it matches exceptions. The speaker illustrates the power of Ruby’s retry logic and the use of ensure clauses for cleanup actions.
    • Discusses dynamic exception matching and the implications of raising exceptions during active exception handling, suggesting using nested exceptions to maintain context for debugging.
  • Structuring Error Handling Strategy:

    • Strategies for avoiding failure cascades, maintaining clear exception semantics in critical methods, and implementing contingency methods to separate error handling from business logic are discussed.
    • Introduces the concept of circuit breakers in distributed systems to manage failures.
  • Best Practices:

    • Discusses the importance of customizing error handling depending on the type of errors, categorized into user errors, logic errors, and transient errors, shaping exception classes accordingly.
    • Concludes with recommendations on maintaining clarity in exception handling code to avoid confusion and improve maintainability.

Conclusion

Grimm underscores that understanding Ruby's exception handling is crucial for developing failure-resilient applications. By applying structured error handling strategies and differentiating between various types of errors, developers can significantly enhance the robustness and maintainability of their code.

He also mentions that some content could not fit into the talk and invites the audience to refer to his book for more detailed information on exception handling.

Exceptional Ruby
Avdi Grimm • New Orleans, LA • Talk

Date: September 29, 2011
Published: December 12, 2011
Announced: unknown

You know how to raise and rescue exceptions. But do you know how they work, and how how to structure a robust error handling strategy for your app? This talk starts with a technical deep dive into Ruby's exception facility, covering features, tricks, and gotchas you never knew about. You'll learn how to retry failed operations; how to override "raise" for fun and profit; and advanced techniques for matching exceptions in rescue clauses. From there, we'll move on to patterns and guidelines for structuring a robust, failure-resilient codebase. We'll look at strategies for avoiding failure cascades; how to characterize and verify the exception-safety of critical methods; and alternatives to raising exceptions for when "fail fast" isn't the right answer. Finally, you'll learn about the three exception classes every app needs.

RubyConf 2011

00:00:17.240 I am obdi and this is a talk about
00:00:22.480 failure hopefully it is a not a not a failure of a talk so to begin with uh I want to uh
00:00:31.240 Define some terms what do we mean by failure one way to look at failure is
00:00:38.440 from the uh the design by contract uh view of programming uh this
00:00:45.640 is the the view of programming that was espoused by bertr mayor uh he kind of encoded it into a programming language
00:00:51.360 called eiel and this is relevant to us because Ruby's exception system is in
00:00:56.719 many ways influenced by Eiffel so in the the design by contract view of
00:01:04.360 software every method has a contract it may be an implicit contract or it may be
00:01:10.200 an explicit contract but it has a contract with its caller saying uh you will give me certain inputs I will give
00:01:16.799 you certain outputs failure is when code is unable
00:01:23.360 to fulfill that contract uh and this could happen for a
00:01:28.560 number of reasons the uh the the codee the method could simply be called with
00:01:35.079 invalid arguments this is technically a failure of the caller but it might be the method's job to report it could be a
00:01:41.960 simple mistake in the code like using a a string key when when a symbol key was
00:01:49.759 expected it might be a case that was simply um not planned for uh when when
00:01:56.759 designing the software or it could be some failure in
00:02:01.840 a system that's completely external to this software that it has no control over like a a web service returning a
00:02:09.119 500 a I want to start off by just taking you
00:02:14.599 on a whirlwind tour of Ruby's exception handling life cycle and uh I'll go
00:02:20.120 through some things you probably know and maybe some things that you don't know so it all starts uh with raising an
00:02:27.120 exception every exception starts with called to raise or to fail raise and fail are synonyms
00:02:33.319 in Ruby uh in recent years raise I think has become more common more commonly
00:02:38.760 used than fail but uh I was I was discussing this with Jim wrick uh and he
00:02:45.800 has an interesting convention where uh he actually uses fail primarily so he uses fail to indicate a failure of the
00:02:53.480 code and then he only uses raay to when he is re raising an exception that he
00:02:59.680 has has previously rescued so I kind of like this convention I've started to use it a little bit more in my own
00:03:06.440 code uh raise with no arguments equivalent to raising a um a runtime
00:03:12.799 error uh raising we can raise of course with a spring which we'll raise a one time error with that string as this
00:03:18.040 message we can raise uh with an explicit error class and uh uh we can also raise uh with we can
00:03:27.080 supply another argument to customize the back Trace uh when we uh when we raise
00:03:33.439 an exception and this is useful when writing things like assertion methods
00:03:38.760 because we want that stack Trace to point back to where the assertion failed not where the assert method was
00:03:47.959 defined now something uh one thing uh some people don't realize about rays in
00:03:53.000 Ruby is that it is a method it is not a keyword so um and that means that like
00:03:58.519 any method in Ruby we can redefine it so here's kind of a stupid example uh
00:04:04.959 we could make all exceptions instantly
00:04:10.360 fatal uh a possibly more useful uh if reimplementation of rays uh is the
00:04:17.359 Hammer Time Jam that I wrote uh so this attempts to be a list or or small talk
00:04:23.600 style uh exception or error console for Ruby so when an exception is raised
00:04:28.720 instead of just being just just unwinding the stack it'll it'll instantly pop up a interactive console asking you if you want to if you want to
00:04:35.000 proceed if you want to ignore the errors if it never happened if you want to debug uh into the source and stuff like
00:04:42.960 that what does raay actually do well it it turns out that raise breaks down into
00:04:49.160 four steps so it goes through uh it first it gets the exception object then
00:04:54.240 it sets the back Trace uh then it sets the global error info variable and
00:04:59.840 finally it uh it it starts unwinding the fal stack let's take a look at these in a little bit more detail uh first of all
00:05:07.160 getting the acception object when you look at the way Rays is called you might think that it's it's defined kind of
00:05:14.400 like this first example where you say uh you know you say raise an exception class in a message so you think okay
00:05:19.800 it's probably saying uh that class do new and then providing the meth the message not actually what ruby does what
00:05:26.160 ruby does is it calls whatever you give it it calls the dot exception method on
00:05:31.639 that object to get the exception object and as it turns out in Ruby uh this
00:05:37.120 method is is defined in two places it's defined on the exception class and it's
00:05:42.400 defined on instances of the exception class but just because Ruby only defines it in one place doesn't mean we can't
00:05:48.520 Define our own here's an example of that uh where we we take uh HTP responses and
00:05:55.919 we give them the ability to turn themselves into exceptions uh which could be you know could be
00:06:01.800 customized a customized exception for that particular type of response and then client code which which can call
00:06:07.840 can do an HTTP request and then uh it can decide whether it wants to to raise
00:06:12.919 that that response as an exception and it doesn't have to explicitly call uh anything to convert it into an exception
00:06:18.960 because raise does that automatically so you can kind of think of exception as similar to 2s or 2 a or 2 I it's kind of
00:06:26.800 like the coercion method to uh to coer an object into an
00:06:32.639 exception step two in Rays is setting the uh back Trace which it does with the set back Trace method on um on
00:06:42.080 exception step three we it sets the global error info variable so this
00:06:47.720 variable is is spelled dollar bang out of the box but if you require the
00:06:53.880 English uh library then it's Alias to the somewhat more readable error info
00:06:59.599 Global This Global always contains the a reference to the currently active
00:07:06.080 exception if any so what this piece of code demonstrates is that prior to
00:07:11.680 raising an exception the error info variable is nil during the raise of that exception
00:07:18.000 so when we raised it and then we're res we're rescuing it the uh variable is the
00:07:23.560 that variable is set to that exception the exception that was raised and then after we've handled the exception after
00:07:29.080 the end of the rescue block then that variable is nil once again and step four
00:07:36.479 uh in raising an exception is to start unwinding the call stack and uh I'm not going to get into the technical details
00:07:42.400 of that once we've raised an exception we
00:07:47.599 handle it with rescue rescue with no arguments uh rescue with no arguments will catch any
00:07:53.000 exception right right uh or wrong rather with no
00:07:59.879 arguments will C capture a anything descended from standard error this
00:08:04.960 leaves out a whole slew of exceptions and this is this is a common like uh point of beginner confusion because uh
00:08:11.159 things like no memory error things like the load error not implemented error and a whole bunch of others uh will go flying right by a be
00:08:18.400 rescue uh you can give you can give rescue just a name but no class it'll assign the exception the uh the
00:08:25.919 exception to that variable you can of course Supply class to to say what class
00:08:30.960 of Errors you want to catch you can also Supply a list of classes uh to say uh
00:08:36.279 catch any of these now when you look at the
00:08:43.080 syntax of a rescue Clause uh it it looks a little familiar it looks kind of like
00:08:50.200 um looks kind of like a case Clause right um and this is not a coincidence
00:08:56.080 Ruby's Logic for matching an exception with a class is actually very similar to
00:09:02.959 its case matching uh it's its case matching logic which if you know how case works it uses the three equals
00:09:09.880 operator you know three equals in a row to do its its matching uh exceptions are matched the exact same
00:09:17.160 way this is interesting knowledge because it means that we could theoretically do uh Dynamic exception
00:09:24.880 matchers which aren't just classes but which are you know which implement the the threequels to do some logic and uh
00:09:31.720 and and just provide those instead of instead of classes now there's one little hiccup in this Ruby has a kind of
00:09:38.519 arbitrary requirement that whatever you pass to rescue must be a class or a module it can't just be any arbitrary
00:09:44.800 object but as long as it's a class or a module uh it just has to implement the three operator so here's some code which
00:09:51.000 generates an anonymous module and uh and sticks a threequels operator on it which
00:09:56.519 uh which is based on some which is based on some uh some pattern matching code and it matches on on the message and now
00:10:02.600 we can say rescue errors with message and rescue any error whose me whose
00:10:08.839 message matches that regular expression uh we there we also have an
00:10:15.120 Ure clause which is where we put code that will always be executed whether an exception is raised or not this is a
00:10:20.760 good place to put cleanup code now Les Hill has documented an interesting uh
00:10:25.880 Corner case with Ensure if you have an explicit return return inside your return your insure clause and an
00:10:32.720 exception is raised that exception will be thrown away without a trace uh it'll just it'll return normally so probably a
00:10:39.320 good idea to just avoid having explicit returns inside your inore Clauses Ruby is one of the few languages
00:10:46.279 that gives us a retry capability so uh here's a really simple demonstration of
00:10:52.160 retry we Define a tries counter uh then we we increment it we try to do some
00:10:58.880 operations which may raise an exception and uh and then in the rescue
00:11:04.320 we look at the tries counter if it's under three we try again we try again by saying retry and what that does is that
00:11:09.800 throws execution back up to right after that uh that first begin block and uh
00:11:15.600 and then tries again and then after three tries uh this code gives up now you could do the same thing uh with a
00:11:23.279 with an explicit Loop for tries but what I find is when I have code that I've already written which hits some kind of
00:11:30.160 external buggy external service or something like that and I realize after the fact that it's that this service
00:11:38.160 that is hitting is unreliable it's often easier to put some retry logic in than to rewrite that code uh with a with a
00:11:46.600 loop what happens when we raise an exception while handling another
00:11:53.200 exception depends on how raise is called so if you call it with uh with a Brand
00:11:58.760 New Me message then uh it will it will just
00:12:04.040 substitute the new exception the new exception will be raised the old one will be thrown away there is no way to
00:12:10.320 get an a reference to the original exception and this tends to complicate debugging a lot uh because you'll you'll
00:12:17.120 Trace back to the origin of an exception and then you'll disc discover that it was raised while handling another
00:12:22.880 exception and you have no idea what that that uh original exception was so please
00:12:30.000 avoid doing this instead use nested exceptions nested exception is simply an exception which carries with a reference
00:12:36.079 to the original exception if any Ruby doesn't have these out of the box but it's very very easy to Define your own
00:12:42.279 here's here's an example it just uh defines a it has a slot in it for the original and you can pass that original
00:12:49.199 that original error in when you're raising it so um we' we've been a little
00:12:55.040 bit clever though with this definition of of a ased exception because if you
00:13:01.160 look at the second example we're raising we're raising the a second a second
00:13:07.680 error but we're not explicitly providing the original to it but it'll actually pick up the original
00:13:14.480 anyway the way it does that is we've said that the default value for that original attribute is dollar bang it's
00:13:21.920 the error info variable which as you recall it always holds a reference to the currently active exception so this
00:13:27.880 way it kind of match you know magically picks up from the environment the exception that was active when it was
00:13:36.639 raised you can rease uh you can re simply rease the exception that you rescued this will throw the exact same
00:13:44.240 object um up the call stack without changing it anyway you can supply a uh a new method
00:13:52.000 you can supply the original exception and a new message and this actually creates a new exception object it doesn't set the message uh it just
00:13:58.360 creates a new exception object but it duplicates that old object so any kind of state that the old object had with it is going to be is going to be duplicated
00:14:05.040 and copied it just uh changes the message on the new one this is actually kind of useful uh
00:14:11.480 so imagine a case where you have a data file and you're loading some data in by
00:14:17.560 it's the data file is just lines of Ruby code and you're loading it by going going through the file line by line
00:14:23.800 evaluating the line and assigning the result to to an right index let's say there's a error some in that file here's
00:14:31.040 the here's the error you get says compile you know syntax error compile error uh and it's just it's just
00:14:39.639 pointing to the first line of that inval statement no idea where in the actual
00:14:45.160 data file that syntax error is but what we can do is we can wrap
00:14:51.680 that that eval in some exception handling code which will it'll rescue any any exception and then it will use
00:14:59.079 this ability to rewrite the message to add some contextual information so in
00:15:04.360 this case we're adding the path and the line number of the file uh where that
00:15:09.880 that error occurred and then just and then it reases the exception but with a new message um
00:15:29.240 that's a good point uh so so in this particular in this particular case yes you you could pass the the line number
00:15:36.279 uh and to uh to eval and and it would accomplish the same goal however um so
00:15:41.880 so that that's for this case but for in general um if you have if you have code
00:15:48.880 um you know where a lowlevel exception may bubble out and you have the ability to it at a higher level to attach extra
00:15:56.600 context which will make debugging that error um easier than uh this is a handy
00:16:04.920 technique uh you can also if you need to you can rewrite the um the back trace on when you re ra this
00:16:12.680 probably an a good area to tread lightly though if you re if you call raise
00:16:18.079 inside rescue with no arguments whatsoever it's just going to re raise the currently active
00:16:23.440 exception um now some languages don't allow any of
00:16:28.560 this some languages you just can't you can't raise during a during handling
00:16:33.800 another exception and there's some there are some good reasons for this I mean um you know as we've as we've seen
00:16:40.079 sometimes ra sometimes raising during a a rescue can complicate debugging uh it
00:16:45.560 can also result in resources not being properly cleaned up if we wanted to uh we have some more fun with overriding
00:16:51.560 rays and Ruby and we could Define a raise which does not um which does not
00:16:57.959 allow re raising within a rescue and uh here's how we use it so we can include it into object and uh when
00:17:05.760 we try to raise a second error while rescuing a first error it just ends the program um however what we've also done
00:17:11.799 is we provided a an error handled method where we can signal that yes you know explicitly yes we have handled it and
00:17:17.720 now we're raising a new one and what this does is it just sets that error info uh variable back to nil so it is it
00:17:24.720 is mutable and uh and then it continues on
00:17:32.160 if an exception is not rescued in Ruby eventually it'll bubble up to the top of the call stack and Ruby will will cause
00:17:39.120 the program to exit um however it won't do this until it has executed various
00:17:45.520 exit handlers uh so I'm not going to go over the the details of the differences between trap and at exit and end uh but
00:17:53.720 the point is all these things can be will be called uh before the program actually
00:17:59.840 exits even in the case of exiting du an exception and and this is kind of Handy
00:18:05.039 um if you ever had an application where you wanted to put some crash logging in
00:18:11.120 it you wanted to log um unhandled exceptions but you didn't there what for for whatever reason there it was not
00:18:16.480 easy to wrap the entire application in a big begin rescue end block we can still
00:18:21.679 tag on a uh a crash logger without having to wrap the whole application we
00:18:27.159 can do this by finding an ad exit block so that'll be that'll be executed at the when program exits and we can use that
00:18:34.159 that handy dandy error info variable to check to see if the program is exiting
00:18:39.440 because of an error and if it's exiting because of an error we write out we write out some information to a log file
00:18:45.320 here so we're writing out some information about the error and in this case we're also writing out the versions of all the ruby gems that were loaded at
00:18:51.440 the time of the error I'm sure you can think of lots of other things you could throw in a in a uh a log file that major
00:19:07.039 all right so that's that's the mechanics um let's talk a little bit about how we respond to failures in
00:19:14.919 programs one way to respond to failure is simply uh to return some sort of error value in Ruby programs more often
00:19:22.520 than not this error value is is simply nil uh a related approach uh which can
00:19:31.159 sometimes uh work better is to return some kind of benign value so you return
00:19:36.400 something which more or less behaves like the object that was expected out of that method uh expected as a return
00:19:43.960 value but it just has some placeholder values and and this is useful sometimes uh when you don't want to when you don't
00:19:50.120 want to force the caller to check the return value U maybe the maybe the method isn't you know too vital and you
00:19:55.600 don't want to bring down the program because because it failed
00:20:00.919 uh you may find yourself uh wanting to log exceptions to some kind of uh some
00:20:06.679 kind of external service you might want to log them to a file you might want to log them to an external service um I
00:20:11.720 think I forgot to rewrite HP toad as uh as air bra but um and this is this is a
00:20:18.200 a great thing to do but you need to be careful that you don't make things worse when you do this so I'll tell you a
00:20:23.760 little story about this um I worked on a project once where we had a bunch of we had a a distributed Cloud uh we had we
00:20:30.799 had you know some master systems and we had cues and we had lots of workers pulling jobs off the cues and processing
00:20:36.799 them and sometimes the the jobs would fail and we had written a little really basic uh error reporting system which
00:20:43.520 would just email us we use our Gmail account to send us an email with the error information and then the you know
00:20:49.480 and then the it would take the next job off the queue and keep going so this worked okay uh but one day we rolled out
00:20:57.600 uh an update with which caused the number of failed jobs to go up quite a
00:21:03.240 bit and so suddenly we're getting you know hundreds and hundreds of of error
00:21:08.520 reports and as a matter of fact we got so many error reports going out to our Gmail account that uh Gmail frauded our
00:21:16.720 Gmail account and the result of This was um
00:21:24.200 the result of this in the code was exceptions it was uh SMTP exceptions
00:21:30.720 being raised that code had never been written to anticipate SMTP exceptions and so as a result the workers instead
00:21:38.360 of instead of reporting the error and then moving on to the next job they would simply fall over and die because
00:21:44.320 the the exception would would Bubble Up and Kill the whole worker but that wasn't the worst of it
00:21:49.400 so we had workers dying but that wasn't the worst we had other systems unrelated systems that also reported their status
00:21:56.360 using our Gmail account and so they were emailing us and then
00:22:01.840 and they hadn't been written to handle SMTP exceptions either and so they were falling over and dying too so we had
00:22:07.200 completely unrelated services to these workers falling over and dying because we had failed to isolate our our
00:22:14.919 exception handling code so it's just something to be aware of uh one strategy
00:22:22.039 for dealing with this this tendency of failures to multiply like this uh is the the circuit breaker pattern which uh
00:22:28.720 niggard describes in his wonderful wonderful book release it I don't have time to go over the pattern in detail
00:22:35.000 but I do recommend if you have uh Distributing systems like this you check that
00:22:41.279 out let talk a little bit about how you structure uh your failure handling
00:22:47.039 strategy in your libraries or applications first of
00:22:52.480 all when how do you make the decision to raise an exception versus um versus
00:22:59.960 doing handling the error some other way the U the simplest the simplest rule
00:23:08.440 for this um but but one of the harder ones to interpret is well exceptions
00:23:13.760 should be exceptional you know only raise exceptions for truly exceptional cases but this can
00:23:21.360 be a little bit hard to can sometimes be a little bit hard to decide what warrants an exception what doesn't um
00:23:28.159 one thing that is not that I don't think is exceptional is invalid user input humans
00:23:34.000 make mistakes they f finger keyboards they misund understand uh uis so I think uh you know I used to I
00:23:42.000 used to look at active record and see how it would not you know by default when you call raise it doesn't or when you call Save sorry uh it doesn't raise
00:23:48.640 an exception uh if the data is invalid and I used to think that doesn't seem right that's not like pure o uh correct
00:23:54.760 and then I thought about it some more and I realized invalid data in that context in the
00:24:00.279 context of of like a web form is not unexpected this is not an unexpected circumstance so these days I actually
00:24:06.400 think it does the right thing because because that's unexpected thing to happen um and and so it actually kind of
00:24:21.320 you really do want to break out of multiple levels of execution the way an exception does but it is not an
00:24:28.440 exceptional case it's not an unexpected case Ruby actually has has a construct just for that it's
00:24:35.200 called throw and catch and a lot of people coming into Ruby get that kind of confused with the raise and rescue um
00:24:41.000 but this gives us a way that is not part of the exception system at all to to bomb out of a piece of code and and and
00:24:47.320 go up to a higher level um you can see an example of this in Sinatra so Sinatra
00:24:53.799 you can say last modified in in an action and it'll check to see if the browser has latest version of of the the
00:25:00.799 the page that it's serving and if so it'll just stop right there no more of that action will be will be executed the
00:25:07.679 way that's implemented is it throws the halt symbol up the call chain and then a
00:25:13.760 middleware that's higher up the call chain catches that halt symbol and uh
00:25:19.000 and ends the request right there so it it avoids any expensive processing that might have been further down the
00:25:24.640 action another question to ask yourself am I prepared to end the program any
00:25:30.880 exception not handled can end the program or in the case of an application server can at least end the request and
00:25:38.120 sometimes just looking at it from this perspective just looking at it from the perspective of do I am I prepared to end
00:25:43.840 the program over this can change your your your way of thinking about a particular case you can think you know
00:25:49.520 what that case actually is it's a secondary functionality maybe we should just fall back in some way and not
00:25:56.000 actually uh not actually raise an exception there question number three I'd like to
00:26:02.720 ask is can I punt the decision into somewhere else so this this question of what
00:26:10.120 constitutes an exception continually plays Us in in programming and and you know you can ask
00:26:16.600 uh is an end ofile a failure is a Miss Missing hash key a failure is a 404 from
00:26:23.399 some web service of failure um and the answer to all of those is it depends
00:26:29.039 it depends on the context it depends on the circumstance but when you raise an exception you force the issue you say I
00:26:34.679 know that this case is unexpected I know this cases is exceptional um and
00:26:40.440 particularly when you do that in library code sometimes um you can be kind of asserting an opinion which which may not
00:26:47.000 match reality so whenever I have a case like this uh where I'm really not sure
00:26:52.880 of what decision to make I like to see if there's a way I can PT the decision somewhere else and um um and you can do
00:26:59.600 this in Ruby and an example of this that I really like is the fetch method uh
00:27:04.960 that a lot of the that arrays and hashes and lot of the Ruby uh containers have where you can specify a fallback action
00:27:12.200 or a fallback value uh if the key that it's looking for is not found and so for an optional key we
00:27:19.559 could specify just a default value that it'll fall back to for a required key in a hash we could specify instead of a
00:27:25.080 default value we can say raise raise some and uh and this way the caller decides
00:27:33.279 on a caseby Case basis whether that missing key repres represents an
00:27:39.039 unexpected case or just a an ordinary expected case uh and we can do this in
00:27:44.720 our own codes it's very simple to do this kind of fallback action where we uh we take a block and we let the caller we
00:27:52.159 pump that decision up to the caller caller decides uh how to handle the case where uh you know where
00:27:58.679 where a value is missing or something didn't work question number four am I am I
00:28:06.919 throwing away valuable Diagnostics so um this is so this is the case where for
00:28:14.360 example where you have some process that that you know maybe you're provisioning a server or something takes 15 minutes
00:28:20.960 um and accumulates all kinds of context along the way and all kinds of logs and
00:28:27.120 uh and and objects are created and then one trivia little error causes an exception to be raised and it blows up
00:28:33.399 through the call stack and throws away all that contextual information cleans it up and and you're left with a very
00:28:38.880 little idea of what was actually going on at the time that the exception was raised and you also don't have any kind of intermediate output or anything like
00:28:45.120 that so this is a case where it might make sense not to raise an exception it might make sense to have just some kind
00:28:50.760 of some way of collecting uh error Flags or or error info uh along the way
00:29:00.080 no uh finally uh a last question I'd like to ask is would continuing forward result in a less informative exception
00:29:07.600 so sometimes I mean particularly when you you're like when you have input to a method you have a case
00:29:14.600 where where it's just you know certain values are simply unacceptable maybe like a nil value for an argument is is
00:29:20.840 not acceptable and if you let it proceed further down into that method it might raise an exception which is pretty uncr
00:29:29.080 uh but if you catch it uh right at the top there if you raise an exception right at the top there with the guard Clause you can uh you can be a lot
00:29:36.440 friendlier to to the caller so that's that's a case for failing
00:29:42.320 early in my code I like to uh as much as possible isolate the exception handling
00:29:49.360 code from the mainline business logic um if you you've probably seen
00:29:58.120 some code that looks like this maybe um you've seen code that looks like this only times 10 so where you have uh begin
00:30:06.000 rescue and then inside that there's another case let begin rescue and then inside that there's another another case
00:30:11.480 all these things that could go wrong and it tends to make the code really hard to
00:30:17.760 follow because you're you're mixing up the the business logic with all these sort of T all these sort of tangents on
00:30:23.679 oh if this goes wrong we have to do this oh if this goes wrong we have to do this uh very hard to follow that kind of
00:30:29.159 logic also in my experience code like that tends to be um tends to be bug
00:30:35.600 written and hard to debug um I've actually started thinking of the begin keyword and Ruby as kind of
00:30:42.080 a code smell because Ruby gives us this neat um this neat syntactical
00:30:48.799 idiom where if you only have one level of failure handling in your method uh
00:30:54.760 just just a top level fa failure handling you can leave out the begin clause and you can
00:31:01.240 just have beginning beginning of your method and then you can have rescue uh
00:31:06.519 and then if you want you can have an inure and then you can have end and so what that does I I I like this because
00:31:11.639 it kind of neatly divides the method into sections you say here's our here's our Mainline business logic up to this
00:31:16.720 point below this this is all this is all error handling and then if needed below this this is house cleaning um and it's
00:31:23.399 it's really nice division um I think it it makes code code easier to follow and it it tends to result in in better
00:31:29.679 Factor code too uh if you're trying to move code in this Direction one thing you can do is
00:31:37.240 use something I call a contingency method so let's say you have let's say you have uh some code that does a lot of
00:31:43.760 IO and every single time you do an IO operation you wrap it with a begin rescue end where you rescue IO error or
00:31:50.440 you rescue system call error and uh and you you handle it pretty much the same way every time uh but each time you do
00:31:56.880 some IO you have to gra it with that because you know sometimes this is going to fail uh what you can do is you can
00:32:04.000 Factor those begin rescue ends out into a contingency method whose only
00:32:09.440 responsibility is implementing your failure handling policy for that code
00:32:15.600 and so now You' you've got you've kind of separated things out into business logic and IO failure handling policy and
00:32:21.720 you can now change those independently um and I think it reads Elan here as well
00:32:30.639 Pop Quiz what parts of this code can raise an exception just yelling
00:32:39.440 out that's right all of it the answer is all of it because in Ruby there's no
00:32:45.039 there are certain exceptions which can be raised at any point in the code so things like no memory error if you run
00:32:50.880 out of memory that's going to be that can be raised anywhere um signal exception somebody presses control C on the keyboard that signal exception pop
00:32:58.120 up anywhere and um this is a little scary
00:33:03.519 uh because sometimes we have methods which are kind of important and we don't you know methods like that like
00:33:09.200 especially like exception reporting methods that we really uh we don't want them to to mess things up further
00:33:15.080 exception is raised um so critical methods need known
00:33:20.399 exception semantics and um and there's actually kind of a a there's a there's
00:33:26.120 an old um there's an old list of of guarantees this has been around for a while I came
00:33:31.880 cross it in the in the C++ Community uh that kind of divides up the the
00:33:37.840 different uh levels of exception safety that a method can have so you start at
00:33:42.880 the lowest level you start with the weak guarantee which just says uh this method promises that the object will be left in
00:33:49.039 a consistent state if an exception is raised might be a different state but it would at least be a consistent state it won't have like null pointers or
00:33:55.240 something like that uh then you have a strong guarantee it's basically a roll back guarantee it says the object will be rolled back to its beginning state if
00:34:02.279 an exception is raised and finally strongest guarantee is the no throw guarantee where you say I promise no exceptions will be raised from this
00:34:08.879 method it is useful at least when thinking about some of your more critical methods to think about them in
00:34:16.440 light of these three guarantees and and think you know
00:34:22.000 what what guarantee should this method uh try to try to promise and and then
00:34:29.960 think about you know how what can I do to ensure that that that it is actually guaranteeing that uh there are some
00:34:36.079 automated methods for verifying your uh your exception guarantees and verifying the method actually does implement the
00:34:41.839 strong guarantee and stuff like that but unfortunately I don't have time to go over them uh in this talk here's an an
00:34:50.159 pattern sometimes you encounter some code that just raises some like client
00:34:55.919 uh Library code or something that just raises all kinds of exceptions uh with no Rhyme
00:35:01.480 or Reason and you finally just wind up wrapping the thing in rescue exception and uh and throwing every exception
00:35:09.640 away unfortunately this sort of this is a huge source of bugs I can't tell you
00:35:15.520 how many times I have debug situations where it turned out that the reason things were uh things weren't working
00:35:22.400 right or or failing to fail properly even it was because they um
00:35:29.280 they did one of these they just threw away all exceptions um if if because of
00:35:34.440 the way the code that you're you're wrapping is written you can't guarant you can't rely on any particular exception class being raised uh if
00:35:42.079 nothing else try and match on the error message and uh and that way you can
00:35:47.200 maybe limit some of those uh some of those surprises when you're writing Library
00:35:54.240 code um you're there's kind of a conundrum around how to structure the exceptions that come out of your library
00:36:00.240 code so on the one hand it would be nice if you could just raise uh you know if you have an i people expect an IO error
00:36:06.760 if you have a system you know and Ruby has sort of built in argument errors um
00:36:11.880 Range errors things like that Ruby has these nice built-in error classes that people understand uh and people you know
00:36:17.079 know how to how to know about them and know how to deal with them so it' be nice to be able to raise these sort of
00:36:22.240 classic Ruby exceptions but on the other hand it would also be nice to be able to say
00:36:29.040 all of my exceptions inherit from this one Library exception class and that way you know and if you're using my library
00:36:35.119 you know you can just rescue that one exception class and they will all be caught and so these these two things
00:36:41.200 seem kind of at odds with each other but um there's actually a way to to resolve
00:36:47.960 that so uh it's using something that that I'm I'm referring to as module
00:36:53.040 tagging so what you can do is you can define a module whose only job is to act as kind of a type tag uh it doesn't
00:36:59.720 actually have to contain anything it's just a type tag what you do is is at the top level of your library you just you
00:37:05.920 you wrap your code in Clauses that look like this where you rescue any exception that
00:37:11.720 comes out and you tag it with that mod that module you just extend it dynamically extended with that module
00:37:16.960 and then you re erase it and that way uh your client code uh can kind of haveit cake and E too it can it can choose
00:37:23.680 either rescue specific exceptions you know uh range eror um or uh or it can
00:37:31.880 um uh or it can rescue your sort of global uh Library exception uh I will
00:37:39.119 I'm going to get to some question time uh
00:37:44.599 shortly one of the the um questions the the big questions that
00:37:51.960 I've sort of gone through in every project that I've done is how I
00:37:57.839 structure my exception hierarchy and there are a lot of ways you could go about this you could you could base your
00:38:03.839 structures on the your your exceptions on the module that they come from so you have different software modules you can
00:38:09.760 say you know this module has its own exception class and this module has its own exception class uh you can base them
00:38:16.599 uh more vertically you can base them on like the layer that an exception comes out of so you can say you can have your lowle exceptions and your high level
00:38:22.760 exceptions um you can even do it by severity if you wanted to uh and and
00:38:27.880 this is sort of Bugg me every time I've done a project it feels like I make the same decision uh over again and I'm
00:38:33.720 never never quite sure if I if I made the right decision uh but what I've started to do recently is I've started
00:38:39.359 instead of looking at it um from the point of view of the architecture of the program I've started to look at it from
00:38:44.680 the point of view of of the the end result why do we break
00:38:52.240 down our exception classes at all why do we raise why do we raise different exception classes why don't we just raise runtime error all the time uh well
00:38:59.480 one reason is we might want to tag some some special information onto an exception but the biggest reason is so
00:39:05.680 that we can structure our rescues to to rescue different types of exception
00:39:11.040 differently and so I've started to sort of look at things backwards and work backwards and say um what are the actual
00:39:17.560 what are the ways that that exceptions um are handled differently so what
00:39:22.760 different ways do we do we typically uh see exceptions handled and I I in in a
00:39:28.400 lot of apps they basically break down into three categories I find so you have your your uh user
00:39:33.599 error um user error like you know a 404 404 or 403 403 page or a invalid data or
00:39:41.079 something like that user error says um you have you have done something wrong you
00:39:46.680 have used the system wrong the only way that you can address it is by changing what you what you did and trying again
00:39:53.599 uh then you have another category of logic errors where the message there is we have done something wrong and there's
00:39:59.960 absolutely nothing you can do about it except maybe record plug and finally you see your your transient errors which
00:40:06.480 says uh you didn't do anything wrong and our code isn't broken uh but we're just
00:40:11.960 experiencing heavy load or we're doing some maintenance or something like that and so what you can do uh is you can
00:40:18.119 wait and try again later and so sort of you sort of see this this breakdown along the lines of what the user how the
00:40:24.720 user whether the user is a is a human or whether client code should um should
00:40:30.720 respond and when you work backwards from that you come up with three basic exception classes uh user error logic
00:40:38.960 error and transient error and I've started to to break my exception hierarchies down uh into these classes
00:40:44.599 and and in my experience I think it it it uh it captures the important
00:40:50.440 differences uh between exceptions in most
00:40:55.720 cases all right uh so I thought of trying to summarize all that um but there's no way so
00:41:02.599 hopefully uh hopefully you got something uh from from part of that talk uh hopefully you learn something
00:41:09.319 new um uh when I was putting together a talk about exceptions I discovered I had
00:41:14.440 so much material I couldn't possibly fit it all in a talk so I wrote a book um and uh there's a there's a uh a discount
00:41:21.920 code you can use um and uh that's it thank you very
00:41:56.240 much for
Explore all talks recorded at RubyConf 2011
+55