Avdi Grimm

Things You Didn't Know About Exceptions

Things You Didn't Know About Exceptions

by Avdi Grimm

In the video titled "Things You Didn't Know About Exceptions," speaker Avdi Grimm presents an enlightening discussion on Ruby's exceptions and failure handling mechanisms during the Rocky Mountain Ruby 2011 conference. The talk aims to educate the audience on various innovative and lesser-known features of exception management in Ruby, as well as provide practical strategies for robust error handling in applications.

Key points include:

- Retry Mechanism: Ruby uniquely allows a retry feature within exception handling, enabling developers to reattempt actions after exceptions under specified conditions, especially useful for flaky services.

- Global Variable $!: This variable holds the currently raised exception, which can be leveraged for logging or handling cleanup processes through at_exit blocks, distinguishing how a program terminates—whether normally or due to unhandled exceptions.

- Nested Exceptions: While Ruby lacks built-in support for nested exceptions, custom exception classes can be defined to reference parent exceptions, improving traceability of errors.

- Modifying Exceptions: Ruby permits re-raising exceptions with modified messages, which adds context and clarity, assisting users in understanding the source of the error.

- Keywords as Methods: Some keywords in Ruby, like 'raise', are actually methods, offering the flexibility to override them for custom behaviors.

- Interactive Debugging: The video discusses using gems to facilitate interactive error consoles, enabling real-time debugging at the moment an exception occurs.

- Dynamic Exception Matching: The flexibility of Ruby's rescue clause allows dynamic generation of exception matching criteria, enhancing the efficiency of error handling.

- Exit Handling: It also outlines how calling 'exit' in Ruby behaves like raising an exception, offering implications for program flow and management of exit handlers.

- Ignoring Exceptions: The creation of methods that effectively ignore exceptions allows smoother flow in applications when deemed appropriate.

Through this comprehensive tour of Ruby's error handling mechanisms, Avdi Grimm encourages developers to adopt a proactive approach to build resilient applications and provides insights into advanced exception handling strategies. As a takeaway, his goal is for attendees to leave with fresh knowledge of Ruby's capabilities, a better understanding of failure management, and to consider how they can apply these principles within their own programming practices.

00:00:10.240 Hello, everyone. I've been given this talk about Ruby, exceptions, and failure handling for a while now. Since this is a very special conference, I thought I would do something a little different this time. Today, I want to go through a few things that you may not have known about exceptions in Ruby. My goal today is for everyone here to go away having learned at least one new thing about exceptions. To get things started, please raise your hand if you have not yet learned anything new about exceptions. Okay, just checking. Let’s get started!
00:00:48.320 Ruby is one of the few languages where the exception mechanism has a retry feature. If you've ever had a piece of code where you need to try something over and over again, perhaps due to a flaky web service, you might have used something like a loop that tries something repeatedly until it hits a maximum threshold. Here's a version of that approach using 'retry'. In this example, we define a tries count and try something out. If it raises an exception, we check if we've exceeded our tries. If not, we say 'retry', which sends execution back to the start of the begin block. We keep doing this until we hit our maximum attempts, and then we give up.
00:01:58.840 Now, has anyone not learned anything new about exceptions? Good! Next, let's talk about the $! (dollar bang) global variable. This global variable always contains a reference to the exception that is currently being raised or handled. If there is no exception being raised, then it is nil. It is also known by the alias 'error_info' if you include or require the English module. Here's a simple example using it. Before an exception is raised, the variable is nil. While the exception is being handled, it holds the current exception, and once it has been handled, it is nil again. This variable is useful for several things.
00:02:48.720 Have you ever wanted to add a crash logger to your application to catch unhandled exceptions? For instance, if it’s not convenient to wrap your entire program in a begin/rescue block, you might want to plug in a crash handler. You can use an at_exit block, which is executed when the program ends. How do you know if the program is ending normally or due to an unhandled exception? That’s where the $! variable comes into play. You can check this variable to determine if the exit was due to an exception and then log relevant information, such as logging the time, details from the exception, and the versions of all the loaded gems at the time of failure. There are many other fun details you could log as well.
00:04:36.960 Next, let's discuss nested exceptions. When we talk about nested exceptions, we refer to an exception that has a reference back to the exception that spawned it. If you rescue one exception and then raise another, the new one can reference the original. Ruby does not have built-in support for this concept, but it’s straightforward to define a custom exception class for that purpose. You can add an original attribute to the class and set it when raising the new exception. Thus, when you inspect the resulting error, you can see both exceptions linked together, even if you didn't explicitly set that reference.
00:05:17.840 Now, has anyone here still not learned anything new about exceptions? Alright! In Ruby, we can re-raise exceptions if we decide that we cannot handle them. However, a less commonly used feature is the ability to modify an exception before propagating it as well. For example, if you are loading data from a file line by line, and it raises an inscrutable exception, you might want to catch it, modify its message, and then raise it again with more context about where it originated. Doing so gives the user additional information to understand the error better, while preserving all the other attributes and states of the exception.
00:06:08.360 Has anyone still not learned anything new? I could stop right here, but I will keep going! One surprising thing for many coming to Ruby is how many keywords are not actually keywords at all — they are methods. For instance, 'raise' is just a method on Kernel. This means we can override it and define our own version. Here’s an example of a rather silly version: suppose we want all exceptions to be fatal immediately. This custom version of 'raise' merely prints out the exception message and then terminates the program. Not saying you should do this, but it's possible!
00:07:22.120 Another interesting use of exceptions is in setting up a Lisp or Smalltalk-style error console in Ruby, which allows developers to interactively debug at the point an error occurs. For instance, there's my 'hammer time' gem that does just that—providing an option to dive directly into a debugger at the point where the exception was raised. It also allows you to raise the exception while giving you interactive context. When raising exceptions in Ruby, you might think that the default 'raise' function works by passing an exception class and message, but instead, it calls the 'exception' method on the class, which essentially coerces the message into an exception object.
00:09:25.280 The 'exception' method acts as a coercion method to convert other objects into exceptions. Ruby actually defines this behavior only on the exception class, integrating a class-level and instance-level for it, depending on how it's called. You can also define your own so that when your response warrants an exception, it can generate one from an object, delegating the decision on what information to include in that exception to the caller. This way, you can craft a flow that allows for informative exceptions based on user-defined responses.
00:10:51.440 Moving on, the way Ruby's rescue clauses are structured looks quite similar to Ruby's case statements. In rescue, you can match on a single class or a list of classes to catch exceptions. Interestingly, this implementation allows us to dynamically generate the list of classes at runtime. You can create a method such as 'ignore_exceptions' that takes a list of exceptions you want to ignore and wraps the code where those exceptions might be raised, passing the list to 'rescue' through argument splatting. This allows efficiency and flexibility in managing exceptions.
00:12:18.760 Now, let's explore how we can become even more creative with rescue. Since rescue is implemented similarly to case statements, you can use the triple equals operator to define custom matching behavior. For example, we could create anonymous modules with custom three-way equals operations to match exceptions based on specific conditions dynamically. By doing this, you could define matchers based on characteristics of exceptions, such as attributes like a retry count, and match any exceptions that have been tried less than three times. This makes handling retries more elegant.
00:14:10.960 So, should we stop there, or do you want more? Thank you! When you call 'exit' in Ruby, you are actually raising an exception. For instance, if you write a piece of code that rescue any exception after calling 'exit', it still allows the program to continue running. It’s a way to prevent programs from exiting unexpectedly. Similarly, 'abort' behaves similarly but sets an error status instead of a success status when raising a system exit. If you want to exit immediately, you need to call 'exit!', which terminates the program without executing exit handlers or any other checks.
00:15:25.200 One last note: if you really dislike handling exceptions, you can create a method that defines how to ignore an exception without modifying the original exception class. This method allows you to call an ignore method on the exception. This would allow your program to continue operating as though the exception wasn't raised, effectively giving you a way to have exceptions that can be ignored contextually based on your modification.
00:16:03.760 Here's a clever modification for the exception class to incorporate continuations, making it more manageable to ignore exceptions by tunneling back to their origin. The 'raise' method is redefined to surround the original raise call with one that captures the context, allowing the program to jump back later as if no exception was actually raised. This modification introduces the idea of continuations in Ruby, where you can define a process to continue code execution from where an exception was caught.
00:18:01.040 Now, does anyone have questions? I still have time left, and I'd be happy to take them. Perhaps you’re curious about any of these ideas or implementations. For those that want to delve deeper, I wrote a book about this topic, and there’s a discount code available for you to use. Additionally, I would appreciate it if you could tweet a thank you to my wife, Stacy, for supporting me through all my talks and conferences.
00:19:24.760 Finally, as we wrap up, don’t hesitate to ask more questions. Have you ever had experiences with Ruby exceptions that you found surprising, or do you want to know more about specific mechanics? I’ve also explored aspects of exceptions in rack and Sinatra applications where throw and catch methods are utilized for flow control without handling exceptional cases. Thank you so much for listening!