Hammertime: An interactive error console for Ruby

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:

squeak-error

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.

stop-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 =&gt; 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]

17 comments

  1. 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

    1. Awesome! This is something of a toy, so I can't spend a lot of time maintaining it; but patches are very, very welcome 🙂

Comments are closed.