Avdi Grimm
Exceptional Ruby
Summarized using AI

Exceptional Ruby

by Avdi Grimm

In this presentation titled "Exceptional Ruby," Avdi Grimm discusses the intricacies of failure handling and exceptions in the Ruby programming language. He begins by exploring the concept of failure within the context of programming, referencing Bertrand Meyer’s design by contract approach, where each method has a contract to fulfill. The talk dives into Ruby’s rich exception handling mechanisms, detailing both foundational knowledge and advanced techniques.

Key points discussed include:
- Understanding Failure: Failure occurs when a method cannot fulfill its contract, whether due to incorrect usage, coding mistakes, or external issues.
- Raising Exceptions: Every exception in Ruby starts with the raise or fail methods. Grimm highlights the nuances of these methods, including customizing backtraces and creating error consoles.
- Handling Exceptions: Through the rescue mechanism, Ruby allows developers to manage exceptions. Notably, exceptions should be reserved for truly exceptional circumstances and not expected situations like invalid input.
- Advanced Techniques: The presentation covers using retry, ensure, and custom exception nesting to manage errors effectively. He warns against losing the context of exceptions by raising new ones without preserving the original.
- Failure Reporting: Effective strategies for error reporting are emphasized, cautioning against failure cascades, where one error can lead to many more. The circuit breaker pattern is introduced as a way to manage repetitive failures in a controlled manner.
- Philosophical Insights: Grimm shares insights on structuring a robust error handling strategy, including five questions to consider before raising exceptions and the importance of maintaining clean code with a focus on exception safety.
- Best Practices: Recommendations include avoiding overly broad rescue clauses, establishing a single base exception for libraries, and ensuring a clear separation between business logic and failure handling.

The talk encapsulates extensive insights into structuring error handling in Ruby applications, concluding with practical advice and an invitation to explore further resources, including an ebook expansion of the presentation. Grimm emphasizes that while managing exceptions, clarity and maintainability are paramount.

00:00:11 Thank you.
00:00:21 So, my name is Avdi, and I want to talk a little bit about failure handling and exceptions in Ruby.
00:00:27 I want to apologize from the start that there is a ton of content in this presentation, and I'm going to run through it pretty fast. Hopefully, we can get all the way through it.
00:00:41 To start out, I want to talk about what failure means.
00:00:50 One good way of looking at that is to consider the design by contract view of programming as espoused by Bertrand Meyer. In this view, every method has a contract with its caller.
00:01:07 The contract states, 'Give me certain inputs, and I'll give you certain outputs or certain side effects.'
00:01:19 Meyer wrote a programming language called Eiffel, which is relevant because Eiffel's exception handling system was strongly influential on Ruby's.
00:01:31 So, in this view of programming as contracts, every method has a contract, and a failure is simply when a method is unable to fulfill its contract.
00:01:46 This failure could happen for several reasons. There could be a mistake in the way the method is called; technically, this isn’t a failure of the method, but it may be the method’s responsibility to report that it was called incorrectly.
00:02:04 There may just be a plain old mistake in the way the method is coded, such as substituting a string for a symbol.
00:02:16 There may also be a misunderstanding or a case that the programmer didn’t account for, or there could be a failure in some external element that's completely outside the program.
00:02:36 So for the next few minutes, I want to give a whirlwind tour of Ruby's exception system.
00:02:50 We will cover some things you may already know, and then some things that you might not.
00:03:03 Every exception starts with a raise.
00:03:11 Actually, it starts with either a call to 'raise' or a call to 'fail'. These two are synonyms, and they both do the exact same thing.
00:03:25 In recent years, I've noticed that 'raise' has become a little more common than 'fail'. It's really just a matter of taste.
00:03:41 I was talking to Jim about this, and he has a convention that I thought was interesting.
00:03:50 He uses 'fail' for most cases to indicate a failure, and then he uses 'raise' only when he is re-raising an exception explicitly.
00:04:01 I kind of like this convention and have been thinking about using it more in my own code.
00:04:17 There are many ways to raise exceptions in Ruby.
00:04:25 We can call 'raise' without any arguments, which will just raise a RuntimeError.
00:04:35 We can also call it with a string, or we can call it with a specific exception class.
00:04:50 If you supply a third argument to 'raise', you can customize the backtrace. This is handy for scenarios like assertions, where you want the backtrace to point to where the assertion was made, not where the assertive method was defined.
00:05:05 Now, 'raise' is not actually a keyword in Ruby. 'Raise' is just a method defined on Kernel.
00:05:15 As we all know, in Ruby, since it's a method, we can redefine it.
00:05:27 There are some fun things you can do with that. Here’s a silly example.
00:05:38 We could create a program where, instead of exceptions bubbling up through the call stack like they normally do, they just instantly exit the program.
00:05:54 A possibly more useful usage of this fact is a gem I created called 'Hammer Time', which is basically a Smalltalk-style error console for Ruby.
00:06:07 Instead of just ending the program, you could get a backtrace at the moment the exception is raised.
00:06:18 You can look at the environment where it was raised, and you can debug right there.
00:06:28 So what does 'raise' actually do? 'Raise' goes through four steps.
00:06:42 The first step is to get the exception object.
00:06:54 The second step is to set the backtrace.
00:07:04 The third step is to set the global error variable.
00:07:11 Finally, the fourth step is to throw that exception up the call chain.
00:07:21 Taking a look at those in a little more detail, getting the exception object, when you look at how you call 'raise', you might think that what's happening internally is something like calling 'ExceptionClass.new' on the class you pass.
00:07:36 However, it’s actually calling a method called 'Exception' to generate that exception object.
00:07:46 In Ruby's built-in Exception class, 'Exception' is defined at both the instance and the class level.
00:08:04 At the instance level, calling it with no arguments just gives you the same object back, while calling it with arguments gives you a duplicate of that exception.
00:08:23 At the class level, it's basically equivalent to calling '.new'.
00:08:34 This is interesting because it means we could actually define our own 'to_exception' methods.
00:08:52 You can almost think of '.exception' as equivalent to 'to_s' or 'to_a'; it’s almost like a way of saying, 'Convert yourself to an exception'.
00:09:03 I haven't really seen this used in practice much, but here’s an example where you could tell an HTTP response to generate its own exception instead of deciding what exception to raise.
00:09:15 Next, we set the backtrace in 'raise'. This is either going to be set from the current backtrace or, if you supply a custom one, it will be that.
00:09:31 This is set on the exception object separately from creating it.
00:09:42 In step three, it sets the global error variable, which is the '$!' variable.
00:09:53 It's not really a global; it's actually a thread-local variable, but it looks like a global.
00:10:05 This little piece of code demonstrates that as long as an exception is active, meaning it hasn't been handled, the global error variable is set.
00:10:20 Once it has been handled, the variable goes back to nil.
00:10:32 If you find the '$!' syntax a bit inscrutable, you can require the 'English' library and call it 'error_info' instead.
00:10:51 Finally, 'raise' tosses that exception up the call chain.
00:10:58 It continues to go up the stack until something either handles it or it reaches the top level of the program.
00:11:10 Now that we've raised an exception, we need to handle it. 'Rescue' is how we do that.
00:11:28 You can call 'rescue' in a number of ways as well.
00:11:40 There are arguments, which are equivalent to rescuing StandardError.
00:11:53 Notably, it won’t catch a lot of exceptions that descend from StandardError.
00:12:04 You can give a name to the exception, and you can also provide specific classes or a list of classes to define what you want to rescue.
00:12:14 When you look at the syntax of 'rescue', you might think it resembles Ruby's case statements.
00:12:29 In fact, the way Ruby decides whether a rescue clause matches an exception is quite similar to how it does case matching.
00:12:41 What’s actually happening is that it's calling the '===' operator between that class and the exception to see if it matches.
00:12:54 That means we could be a little creative with this too; instead of providing a class, we could create a custom matcher function.
00:13:07 We could then say something like 'rescue errors with message matching this regular expression'.
00:13:24 However, one little gotcha here is that for some reason, Ruby requires that whatever we pass to 'rescue' must either be a class or a module.
00:13:39 It just calls the '===' operator on it, not actually checking its class or module status.
00:13:49 This is why, in this code, I am creating a new module that just pretends to be a class or module type.
00:13:59 I'm not entirely certain if this works in MRI; probably not, I think.
00:14:14 After using 'rescue', you can also add an 'ensure' clause.
00:14:27 This is good because everything in the 'ensure' clause will always be executed, error or not.
00:14:39 It’s a good place to put cleanup code.
00:14:53 One gotcha with the 'ensure' clause that was documented by Les Hill is that if you explicitly return, the exception may be effectively thrown away.
00:15:07 The exception will not be propagated up, which may not be what you expect.
00:15:20 It's probably best to simply avoid using explicit returns in that scope.
00:15:33 Ruby is one of the few languages that gives us a 'retry' for exceptions.
00:15:44 'Retry' allows us to say, during exception handling, to go back to the enclosing begin or to the beginning of that method and try again.
00:15:55 The key thing to be careful of when using 'retry' is to avoid getting into an infinite retry loop.
00:16:05 You need to have some type of counter or mechanism in place to decide when enough is enough and that you should give up.
00:16:15 Now, what happens when we raise a new exception during the handling of another exception?
00:16:32 If we just raise a brand new exception, the original exception gets thrown away.
00:16:41 There will be no record of its existence, and you will have no idea what it was.
00:16:54 I've found out the hard way that Rails code often does this.
00:17:08 As you try to track an exception back to its source, you may realize it was generated while another exception was being handled.
00:17:21 You will find yourself completely unaware of what the original exception was.
00:17:34 So please don’t do this.
00:17:44 Instead, utilize nested exceptions. The nested exception pattern is simply an exception object with an additional reference to the original exception.
00:17:59 This isn't part of Ruby but is very easy to define your own nested exception class.
00:18:12 This example is a little bit clever as it uses the global error variable as the default for the original exception.
00:18:25 It auto-detects if there was an active exception while playing with 'raise'.
00:18:39 You can take the error you caught and re-raise it; calling 'raise' on that will raise the exact same object.
00:18:55 This is not going to generate a new object, which is what this code demonstrates.
00:19:07 However, you can also call 'raise' on the exception you caught and provide a new message.
00:19:20 This can be useful to clarify the message when you know more context information and want to add that to the exception.
00:19:36 You can also provide a custom backtrace as well.
00:19:50 Calling 'raise' with no arguments will re-raise the current active exception.
00:20:02 If you raise with the caught exception, does that preserve the backtrace?
00:20:20 The question is if you explicitly re-raise as opposed to raising with no arguments, does this preserve the backtrace? I believe they're semantically identical.
00:20:36 Here’s some more fun with redefining 'raise': in some languages, it’s considered impermissible to do this double raise thing.
00:20:52 When you raise an exception while handling another one, it can lead to difficulties in debugging.
00:21:01 You can wind up not cleaning up resources, which is crucial.
00:21:15 If you wanted to mimic one of those languages, you could easily redefine 'raise' to check if there’s an active exception.
00:21:28 It would then refuse to allow raising while there’s an exception, instead exiting the program.
00:21:42 In this code, I’ve also defined a little helper method that allows us to say explicitly, 'I have handled this exception, and now I’m raising a new one.'
00:21:56 What that does is set the global error variable back to nil.
00:22:10 If an exception continues to bubble up the call stack without being rescued, eventually Ruby will catch it and terminate the program.
00:22:25 Before terminating, it will execute various exit hooks.
00:22:39 This is useful because in some contexts, you might want to capture information about a crash without wrapping a large begin-rescue around your entire program.
00:22:51 Wherever you put that in a Rails app, you can still attach a handler that performs crash logging.
00:23:05 Here’s a simple crash logger that is triggered on exit and checks the global error variable to see if an active exception exists.
00:23:21 If so, it records information based on that exception, including the timestamp, message, and backtrace.
00:23:37 In this case, I’m also logging all versions of all the gems that were loaded at the time of the crash.
00:23:49 I'm sure you can think of additional information that would be valuable to include in a crash log.
00:24:05 Once we've rescued an exception, various handling strategies are available.
00:24:18 For example, if we decide it’s not a fatal issue, we could return an error value.
00:24:31 In Ruby, the typical error value is nil, but nil can be quite uncommunicative.
00:24:44 A related approach would be to return a benign value instead of nil, which is somewhat underused.
00:25:01 This is useful when you've established that the exception doesn’t warrant ending the program or request.
00:25:19 You could substitute a known-safe value to indicate a problem without disrupting the caller.
00:25:31 Another option in handling exceptions is to report them—log to a file, send an email, or notify an external service.
00:25:45 However, you want to be cautious about making the problem worse from within.
00:26:02 For example, I once worked on a project where we had a simple exception notifier.
00:26:20 If a job crashed, it would send an email using our Gmail account.
00:26:38 This worked fine until we deployed a version that caused jobs to crash much more frequently.
00:26:52 The notifications became so frequent that Gmail started throttling our account, causing SMTP errors.
00:27:06 The workers, instead of logging the exception, began to crash heavily.
00:27:20 This didn’t just impact the workers; other unrelated systems that used the same kind of email notification began to encounter errors.
00:27:33 Thus began a classic example of a failure cascade.
00:27:49 It's crucial to be cautious as you enhance your error reporting.
00:28:00 A useful pattern to mitigate failure cascades is the circuit breaker pattern, as described in Michael Nygard's book, 'Release It'.
00:28:12 I won't have time to explore this pattern in detail, but the basic idea is to implement a system that counts failures.
00:28:27 When failures exceed a threshold, the circuit breaker trips.
00:28:40 From there, the system cannot operate for a time, either until timeout expires or a human intervenes.
00:28:56 That was our whirlwind tour of exception handling in Ruby.
00:29:13 For the remainder of this talk, I want to discuss some philosophical ideas and advice for structuring your app or library's failure handling strategy.
00:29:26 First, here’s a general rule: exceptions should be for exceptional circumstances.
00:29:43 If you expect something to happen, like invalid user input, it shouldn't trigger an exception.
00:29:56 You can see this mindset in ActiveRecord’s 'save' method; it doesn't raise an exception when there is invalid user input.
00:30:09 A helpful rule of thumb for deciding when to raise an exception comes from 'The Pragmatic Programmer'.
00:30:22 Ask yourself, 'If I removed all of my exception handling code, would my app still run?'
00:30:39 If the app’s operation depends on exception handling, you may want to rethink your failure handling strategy.
00:30:51 Sometimes, you may want to try to break out of multiple levels of execution.
00:31:06 For those scenarios, Ruby offers 'catch' and 'throw', which can be confusing for those coming from other languages.
00:31:20 These terms are for non-exceptional circumstances but let you break out of multiple levels of execution.
00:31:33 The last modified demand is a good example, interacting with the browser to determine if it has the latest version of a resource.
00:31:48 Internally, it checks browser headers and uses 'throw' to terminate the action beforehand.
00:32:02 There exists an ongoing debate about what constitutes an exception - is an EOF (end of file) a failure? What about a missing hash key?
00:32:15 Ultimately, it depends on the circumstances.
00:32:31 When raising an exception, you enforce the severity of the issue, declaring it a failure.
00:32:45 In programming situations, I often seek ways to defer the decision to someone else.
00:32:59 One way to do this is through a caller-defined failure strategy, which you can see in Ruby’s 'fetch' method.
00:33:11 'Fetch' is defined for hashes and arrays; you pass it a key.
00:33:25 If the key exists, it returns the value, but if not, it runs a block of code to define what to do instead.
00:33:37 This allows callers to decide whether to return a benign value or raise an exception, adding flexibility.
00:33:50 In earlier versions of this talk, people asked me when they should raise an exception.
00:34:01 I've come up with five questions to help determine if raising an exception is appropriate.
00:34:13 First, is the situation truly unexpected? Can we reasonably expect it to happen?
00:34:26 Second, am I prepared for the program to end?
00:34:38 Any exception can potentially terminate a program.
00:34:50 Third, can I delegate the decision-making up the call chain?
00:35:02 Fourth, am I discarding valuable diagnostics?
00:35:13 If an operation is costly or difficult to replicate, raising an exception for a trivial reason may waste vital information.
00:35:26 Fifth, will continuing forward result in less useful exceptions?
00:35:40 In cases of bad input, it’s often best to detect it as early as possible.
00:35:52 Throwing exceptions can complicate code. This is a common complaint about exceptions, as they can lead to spaghetti-like code.
00:36:05 If you see nested 'begin' blocks, they are often some of the buggiest and hardest to understand code.
00:36:18 I consider such occurrences a code smell, something I actively avoid.
00:36:31 Ruby offers an elegant way to avoid complicated 'begin-rescue-end' blocks.
00:36:43 Every method has an implicit 'begin' block that commences at the beginning of the method.
00:36:57 By using this implicit block and adding a 'rescue', we create a clean separation of business logic and failure handling.
00:37:12 It results in more understandable and testable code.
00:37:25 You can refactor code extensively using handling methods, which is a way to extract the failure policy into its own method.
00:37:39 This method yields to the block and executes whatever failure handling policy you've defined.
00:37:51 You can take code that requires certain exceptions to be handled and factor that handling out into a single method to use widely.
00:38:06 Certain methods in a program are critical, such as the crash logger we discussed earlier.
00:38:21 You want this code to function reliably.
00:38:35 When evaluating critical methods, consider their level of exception safety.
00:38:51 Exception safety refers to how a method behaves in the presence of exceptions.
00:39:05 Three classical levels exist: the weak guarantee, the strong guarantee, and the no-throw guarantee.
00:39:17 The weak guarantee means that the object will remain consistent, while the strong guarantee means it rolls back to its original form.
00:39:30 The no-throw guarantee indicates that no exceptions propagate from the method.
00:39:44 In this code block, how many points could raise an exception?
00:39:58 Ruby provides no guarantees on where exceptions might arise; they can occur anywhere.
00:40:06 Stopping execution might happen with a signal exception, for instance, by pressing Ctrl-C.
00:40:18 If the program runs out of memory, that can likewise come from anywhere in your code.
00:40:36 We have a conundrum: we know certain methods are critical; we want to ensure their exception safety.
00:40:52 Yet Ruby does not offer a clear path to predict where exceptions will surface.
00:41:04 We need a way to test whether a method meets the intended exception safety guarantees.
00:41:17 We can create a test harness and execute the code under test, recording every point where external methods are called.
00:41:30 This harness tests the code to ensure that it's either in its original state or fully swapped, never left in an intermediate state.
00:41:43 The harness plays back the testing code, forcing exceptions from the points recorded.
00:41:58 Unfortunately, I lack time to describe how to implement this in Ruby.
00:42:07 However, I have posted a proof of concept that you can refer to.
00:42:17 Being excessively vague when rescuing exceptions can lead to difficulty.
00:42:27 You might catch an unexpected exception and throw it away, causing you to lose sight of issues.
00:42:42 If some code isn’t raising a specific enough exception, you can match based on the exception's message.
00:42:57 Another recommendation is that libraries should base all exceptions on a single base class.
00:43:11 This allows users to rescue the base class without worrying about other exceptions.
00:43:23 I think I'm about out of time. We made it through quite a lot.
00:43:39 Thank you for listening. I hope you gained some insights from this talk.
00:43:53 There are notes on all the references I made—books, slides, and more—at the URL.
00:44:02 There’s also a longer recording of this talk for you to check out.
00:44:15 Lastly, I've turned this talk into an ebook currently in beta, which covers much of what I couldn’t present in the allotted time.
00:44:29 That's it; thank you very much.
Explore all talks recorded at Ruby on Ales 2011
+8