Users of Lisp and Smalltalk environments are used to having some pretty powerful tools for debugging exceptions in their code. For instance, here’s the dialog I see when I try to do execute some bad code in Squeak:
Ruby users are not so lucky. Our first indication of an error is either the code not working or a stack trace, depending on whether the error is handled somewhere. Either way, we don’t get an opportunity to investigate the circumstances of the error when it is raised. All we get to see is the aftermath.
Ruby exceptions are raised with the raise
method, or with fail
, which is an alias for raise
. I use the term “method” deliberately. raise
is not a keyword; it’s a method on Kernel
just like puts
and exit
.
The fact that raise
is just an ordinary method has some interesting implications. For instance, we can modify it to trace the location of every error raised in a program:
module MyRaise def raise(*args) puts "Error at #{caller.first}!" super(*args) end end class Object include MyRaise end raise "blah" rescue nil # => nil # >> Error at -:12!
Since MyRaise
is included in Object
after the default Kernel
, our definition of raise
takes precedence.
Obviously, if we can intercept errors at the moment they are raised, there’s a lot more we can do beyond mere tracing. Introducing Hammertime.
Hammertime is an interactive error console for Ruby. Using it is simple: just gem install hammertime and require the library:
require 'hammertime'
Now when an error is raised, we’re be presented with a menu:
=== Stop! Hammertime. === An error has occurred at example.rb:6:in `faulty_method' The error is: #<RuntimeError : Oh no!> 1. Continue (process the exception normally) 2. Ignore (proceed without raising an exception) 3. Permit by type (don't ask about future errors of this type) 4. Permit by line (don't ask about future errors raised from this point) 5. Backtrace (show the call stack leading up to the error) 6. Debug (start a debugger) 7. Console (start an IRB session) What now?
With Hammertime we can diagnose and fix errors at the point where they occur. Let’s walk through an example Hammertime session using the following highly contrived script:
$broken = true def faulty_method raise "Oh no!" if $broken end 3.times do |n| puts "Attempt (#{n+1}/3)" begin faulty_method puts "No error raised" rescue => error puts "Error raised: #{error.inspect}" end end
We start the code with Hammertime enabled:
$ ruby -rhammertime example.rb
The code raises an error, and we see the Hammertime menu:
Attempt (1/3) === Stop! Hammertime. === An error has occurred at example.rb:6:in `faulty_method' The error is: #<runtimeerror : Oh no!> 1. Continue (process the exception normally) 2. Ignore (proceed without raising an exception) 3. Permit by type (don't ask about future errors of this type) 4. Permit by line (don't ask about future errors raised from this point) 5. Backtrace (show the call stack leading up to the error) 6. Debug (start a debugger) 7. Console (start an IRB session) What now?
We choose “Backtrace” to see the full trace leading up to the error:
5 example.rb:6:in `faulty_method' example.rb:12 example.rb:9:in `times' example.rb:9 1. Continue (process the exception normally) 2. Ignore (proceed without raising an exception) 3. Permit by type (don't ask about future errors of this type) 4. Permit by line (don't ask about future errors raised from this point) 5. Backtrace (show the call stack leading up to the error) 6. Debug (start a debugger) 7. Console (start an IRB session) What now?
Note that Hammertime returns us to the menu after showing the stack trace. Next we choose “Console” to drop into an IRB session. From there, we do a little snooping around:
>> $broken => true
Well that seems to be the problem, someone left the “more magic” switch off. Let’s fix that:
>> $broken=false => false >> exit 1. Continue (process the exception normally) 2. Ignore (proceed without raising an exception) 3. Permit by type (don't ask about future errors of this type) 4. Permit by line (don't ask about future errors raised from this point) 5. Backtrace (show the call stack leading up to the error) 6. Debug (start a debugger) 7. Console (start an IRB session) What now?
And now we’ll choose to Ignore the error and proceed:
What now? 2 No error raised Attempt (2/3) No error raised Attempt (3/3) No error raised
We’ve fixed the “bug”, so successive runs execute without problems.
Hammertime has other features, including the ability to specify certain errors which won’t trigger the menu to be presented. If you want to learn more about it I suggest installing it, playing around with it, and reading the code.
The biggest limitation of Hammertime is that it can only catch errors which are raised within Ruby code, not those raised in native code. I threw it together in an afternoon, so it probably has other bugs as well. Please let me know if you get some use out of the tool. Patches, bug reports, and suggestions are welcome.
UPDATE: Magnus Holm was inspired by this post to create an Exception#continue method. With a little more work I think this could be the basis for a Lisp-style conditions system, something I’d love to see implemented in Ruby.
[ad#PostInline]
Magnus Holm has used this to implement Exception#'continue.
If you didn't make the gem have a hard dependency on ruby-debug, it would install and run fine on JRuby. JRuby has a separate ruby-debug gem that's tricky to install, but which we'll ship with JRuby 1.5.
Note that the stock ruby-debug doesn't work on any implementation except MRI.
http://gist.github.com/282136
Awesome! This is something of a toy, so I can't spend a lot of time maintaining it; but patches are very, very welcome 🙂
Pull request sent!
Version 0.0.2 released!
great tool you got here!
great tool you got here!
Hi Avdi, I think I might have a useful implementation of the Lisp-style conditions you are looking for. It’s very minimal, and adds very little to the existing Exceptions class. It was inspired by a slideshow you gave that is still on the web.
https://github.com/michaeljbishop/mulligan
I’ll take a look!