There's More to Ruby Debugging Than puts()

"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." - Brian W. Kernighan

Debugging is always challenging, and as programmers we can easily spend a good chunk of every day just trying to figure out what is going on with our code. Where exactly has a method been overwritten or defined in the first place? What does the inheritance chain look like for this object? Which methods are available to call from this context?

This article will take you through some under-utilized convenience methods in Ruby which will make answering these questions a little easier.

#class

Calling #class on an instance will reveal what type of Object it is. This can be useful for verifying what classes you're dealing with, and if an instance has decided it's OK to masquerade as another class.


require 'forwardable'

class OptionsHash
  attr_reader :hash

  extend Forwardable
  def_delegators :@hash, *(Hash.instance_methods - Object.instance_methods)

  def initialize(*args)
    @hash = args.first.is_a?(Hash) ? args.first : Hash.new(*args)
  end

  def ==(other)
    @hash == other
  end
end

real_hash = Hash.new
my_hash = OptionsHash.new(real_hash)

puts my_hash == real_hash #=> true
puts my_hash.class == real_hash.class #=> false

#is_a?, #kind_of?

These methods allow you to verify if an instance is a specific class, one of its superclasses, or if its a module thats been included by the class of that instance.

#instance_of?

#instance_of verifies if an instance is a specific class.

#methods, #public_methods, #protected_methods, #private_methods, #singleton_methods

#methods returns a list of all of the public and protected methods on an instance. This includes all methods defined directly on its class, or by those of its ancestors or modules. #public_methods, #protected_methods, #private_methods, and #singleton_methods limit the returned list of methods to their specific scope.

A word of caution: these methods will return all of the methods that have been inherited by the instance. You'll almost always want to filter them by subtracting the corresponding method types from Object or another base ancestor, such as ActiveRecord::Base in Rails.

#method

#method is very useful. It enables us to inspect a method's signature, source location and many other details. This can help with:

  • Narrowing down precisely where a method has been overridden (#source_location)
  • Casting a method to a Proc for use elsewhere (#to_proc)
  • Determining how many arguments a method takes (#arity), and what they're named (#parameters)
  • In the case of aliased methods (#original_name)

#caller

#caller is extremely useful. It allows you to look at exactly what the chain of methods was that called your current method, and exactly what files to look in to see them.

#instance_variables, #instance_variable_get, #instance_variable_set, #instance_variable_defined?, #remove_instance_variable

#instance_variables allows us to peek into an instance and reveal information about its current state. #instance_variable_get and #instance_variable_set provide an external way to manipulate an object's internal state.

Working with Classes

Classes are Objects, just like everything else in Ruby. This means we can define, and inspect them in similar ways. In addition to the previous section there are a few additional methods available to us. Try to keep in mind that a specific class is just a descendant of the Class object.

::name

::name returns the name of the class in String form.

::class_variables, ::class_variable_get, ::class_variable_set, ::class_variable_defined?, ::remove_class_variable

Similar to the #instance_variables methods, these methods allow us to see what variables have been defined specifically on this class as well as manipulate those class variables.

::instance_methods, ::instance_method

::instance_methods will return a list of methods that will be available on an instance of the class. ::instance_method is almost the same as its #method counterpart. The difference is that it returns an unbound method, which means it does not have a receiver and is not actually callable yet.

::included_modules

This provides a list of the modules that have been included in the class already.

::superclass

This provides the immediate parent for the class.

Working with Modules

Modules add another layer of flexibility for providing reusable components shared by many classes. There are some specific methods available which can help when debugging.

Module.nesting

This provides a list of modules, in order, that encapsulate the current module. This is useful for figuring out Class and Module naming conflicts, as well as the order that Classes will be looked up. This method cannot be evaluated externally. It has to be evaluated from within the context you wish to inspect.

::ancestors

This provides a list of all modules included in the class, in order of the inheritence chain.

::constants, ::const_get, ::const_set, ::const_defined?

These are the same as the similarly named #instance_variables, #instance_variable_get, and #instance_variable_set methods, but for constants.

Working with Debuggers

Debuggers can provide a lot of very powerful tools for inspecting running Ruby code. Debugging code without a proper interactive debugger is the worst experience you can ever have. A good knowledge of the debugger can be all the difference in solving a problem in a few minutes, rather than hours or even days.

The basic debugger in Ruby provides a simple IRB interface for interacting with code live. For Ruby before 2.0 you'll want to add gem "debugger" to your Gemfile. For Ruby 2.0 and newer you'll want to add gem "byebug".

The following section will explain a few of the most useful methods available to you while in the debuggers mentioned above.

var

var allows us to inspect the current scope of variables. You can pass in all, local, global, or instance, and it will return the names and values of the variables that have been defined.

backtrace

Very similar to #caller, backtrace provides the current list of available frames and their locations.

frame

frame provides your current location in the backtrace. frame also accepts an integer as argument and will move the debugger to that part of the backtrace, allowing you to inspect the scope higher up the stack.

help

help will list out your available commands. Use help <method> to get information for a specific method.

Closing Remarks

Ruby is amazingly powerful, and provides awesome tools to allow you to building amazing things. The amount of flexibility provided to developers has spawned a vast ecosystem of invaluable code. It has also, on occasion, created situations where debugging can be difficult.

With the tools above you should be much better equipped to debug these situations. Of particular interest is the frame navigation in debuggers, since navigating to a frame allows you to inspect the state it was in at the time it was evaluated. This can be a great ally in detecting the location where a change was introduced in the call stack, enabling you to inspect the offending code.

View original comments on this article