Explained: binding.pry vs byebug vs debugger

Jan. 9, 2022

I wrote this in 2019 to share with my team at work as a few people had mentioned being confused about Ruby debugging tooling. I am sharing it here as the information is generally useful.

There are a couple of different ways you might see a debugger invoked in Rubystan. The two major ones are byebug and binding.pry but a rare debugger is also seen from time to time. I will try to briefly explain the difference between the three.

Binding

To understand the debugging landscape in Rubystan, we must first understand this mysterious class called Binding. This class is used to encapsulate the current “execution context” of the program at any given point. The execution context can roughly be understood as a single frame in the program's execution stack, so it gives you access to all constants, variables and methods accessible in the current scope.

In Rubystan, Kernel#binding returns the Binding for the current execution context. As the Kernel module is included by the Object class, this method is available to be called at any point in a Ruby program.

Binding defines an eval instance method which can take an arbitrary Ruby program, and execute it in the context encapsulated by the Binding instance.

class Foo
  def initialize
    @a = 5
  end

  def get_binding
    binding # binding is a private method
  end
end

foo = Foo.new
foo.get_binding.eval("@a + 1")
#=> 6

This class is the basis of all debuggers and introspectors we will talk about. If you're wondering: yes, it's possible to get the binding from an instance of Binding itself similarly.

binding.pry

Now that we have established what binding does in our code, it's easier to determine what binding.pry does. It just calls the method pry on an instance of Binding!

The gem pry is a drop-in replacement for the IRB REPL that ships with Ruby by default. Pry provides some extra niceties around the REPL, including the ability to move in and out of the context of objects, save the command history to a file, inspect the source of a method by name etc. Pry (like IRB), makes heavy use of Binding to accomplish all this. In this very early version of pry, you can see that the .repl method took a Binding instance as the only argument.

In order to make our lives easier, pry also adds a #pry method on Object to open a REPL session anywhere in our program (much like a debugger, but not quite). This REPL session gives us the same capabilities as a normal REPL but it evaluates the code in the context of the current Binding. When invoked from the terminal via the executable, it starts the REPL in the context of the TOPLEVEL_BINDING, which is a special Binding instance that points to the “top level” context. You can think of it as main in other languages. In fact, if you open an irb or pry session and execute self, you will see that it's actually called main as well.

Coming back to binding.pry, when you put that line in your code somewhere, all it does is call pry on the current binding for that scope, which opens a REPL session within that execution context. As mentioned previously, this is close to a debugger, but not quite. All you can do with this is execute code (like inspecting the variables in the scope), and then exit. There's no stepping functionality available.

# after a binding.pry call
[8] pry(main)> step
NameError: undefined local variable or method `step' for main:Object

However, that's not the complete truth. We may be able to make binding.pry behave like a debugger and use it to step through our code. But to get there, we have to look at byebug first.

Byebug

So we have a REPL, but we want to be able to step through code for it to be useful as a debugger. This is where byebug comes in. It's a gem which, like pry, uses Binding to provide powerful introspection functionality for our code. However, it also provides us with the ability to step through code with step (step into), next (step through), continue (continue execution to the end of the stack) etc.

To access this, we just put byebug somewhere in our code and it takes care of grabbing the current binding, initializing a REPL and starting the debugging session for us. byebug is also a method that the gem adds on the Kernel module so that it's accessible everywhere.

In a default Ruby environment, Byebug integrates with irb as the REPL as irb is available by default with Ruby. But we just discussed the awesomeness of pry, so here comes…

pry-byebug

As byebug doesn't care what REPL you're using, it's advantageous to use the best one (read: pry). The pry-byebug gem takes care of this by extending Byebug's command processor with pry.

So if you have pry, byebug and pry-byebug installed (pry-byebug installs pry and byebug as dependencies, mind you), every time you debug using byebug, you will get a pry powered REPL for your debugging session.

binding.pry As a debugger

And this is how binding.pry gets the capability to step through code as a debugger. It's byebug behind the scenes, via pry-byebug.

So what's the difference between binding.pry and byebug, again?

As we saw, there's a lot of difference between them in isolation, however if you set them up together along with pry-byebug, the differences pretty much disappear. The only difference that remains is that with byebug, you get n, s, c etc as shortcut commands to execute next, step and continue, respectively. With binding.pry, pry-byebug disables these shortcuts as they might conflict with local variable names.

However, it's good to keep in mind that binding.pry is not byebug. It's not even a debugger by itself, and only becomes one by the grace of pry-byebug.

Wait, what about the debugger command?

There used to be a gem called debugger in the Ruby ecosystem, which did for Rubies older than 2.0 what byebug does for us now. However, that gem has been long dead as the Ruby community has almost completely migrated to Ruby 2+.

In Ruby 2, debugger was an alias for byebug that came with the byebug gem.

Starting with Ruby 3.1, Ruby started to ship with a built-in debugger called… debug! This new gem now includes a debugger method for invoking the debugger much like the old debugger gem (not the only alias as you can also invoke the debugger with binding.break or binding.b). Confusion abounds! (Thanks to @jrochkind for pointing me to this update)

Conclusion

binding.pry != binding.pry (with pry-byebug, no shortcuts) == byebug == debugger

It's all more complicated than it should be.


Share this post on :
Posted under code ruby