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!