A Sneak Peek of Ruby's New Debugger!
Tags: ruby, debug, rails, showdev
Published: 2021-07-29 15:22:16

{% github ruby/debug no-readme %}

[`debug`](https://github.com/ruby/debug) is Ruby's new debugger and will be included in Ruby 3.1. Since I've been both contributing to and using it for a while, I feel it's time to give you guys a sneak peek before its `1.0` release 🙂

(*Since it's not officially released yet, any feature mentioned in this article could still be modified/removed in the released version*)


(**Update**: The project's lead developer @ko1 has started a blog series about the debugger. Please also check it 😉)
{% link ko1/debug-gem-blog-initial-commit-3ip %}

## Introduction
As I have mentioned, it's planned to be a standard library of Ruby 3.1. And currently, you can install it as a gem, like:

```
$ gem install debug --pre
```

or 

```ruby
# Gemfile
# it's under active development, so I suggest using GitHub as source when possible
gem "debug", github: "ruby/debug" 
```

Functionality-wise, `debug` is similar to the famous `GDB` debugger and Ruby's `byebug` gem. It provides a rich set of [debug commands](https://github.com/ruby/debug#debug-command-on-the-debug-console) and has some unique features. 

Quoted from its README:

```
New debug.rb has several advantages:

-   Fast: No performance penalty on non-stepping mode and non-breakpoints.
-   Remote debugging: Support remote debugging natively.
    -   UNIX domain socket
    -   TCP/IP
    -   VSCode/DAP integration (VSCode rdbg Ruby Debugger - Visual Studio Marketplace)
-   Extensible: application can introduce debugging support with several ways:
    -   By `rdbg` command
    -   By loading libraries with `-r` command line option
    -   By calling Ruby's method explicitly
-   Misc
    -   Support threads (almost done) and ractors (TODO).
    -   Support suspending and entering to the console debugging with `Ctrl-C` at most of timing.
    -   Show parameters on backtrace command.
```

And these are my favorite features:

- It's colorized.

![Colorize Example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hxc3ylweoqelut9rddsi.png)

- When showing backtrace with the `backtrace` command, it also shows method arguments, block arguments, and the return value.

```
=>#0    Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
  #1    block {|ten=10|} in second_call at target.rb:8
```

- It's possible to script your debug commands with `binding.break` and reduce manual operations. (See the combinations section for examples)
- There are several commands to set breakpoints that trigger under different conditions, like `break`, `catch`, and `watch`.

## `binding.break` (alias: `binding.b`)

If you're a heavy `pry` user like me, you can use a familiar `binding.break` (or just `binding.b`) to kick off the debug session as usual.

But `binding.break` is actually more powerful than `binding.pry`, because it can take commands! 

For example:

- `binding.b(do: "catch CustomException")` - debugger will execute the command (`catch customExeption`) and continue the program.
- `binding.b(pre: "catch CustomException")` - debugger will execute the command (`catch customExeption`) and stop at the line.

(To execute multiple commands, use `;;` as the separator: `"cmd1 ;; cmd2 ;; cmd3"`)

## Fequently Used Commands

The new debugger has many powerful commands. And here are the ones I use the most:

### `break` (alias: `b`)

```ruby
class A
  def foo; end
  def self.bar; end
end

class B < A; end
class C < A; end

B.bar
C.bar

b1 = B.new
b2 = B.new
c = C.new

b1.foo
b2.foo
c.foo
```

#### Basic Usages

- `b A#foo` - stops when `b1.foo`, `b2.foo`, and `c.foo` is called
- `b A.bar` - stops when `B.bar` and `C.bar` is called
- `b B#foo` - stops when `b1.foo` and `b2.foo` is called
- `b B.bar` - stops when `B.bar` is called
- `b b1.foo` - stops when `b1.foo` is called

#### Commands

- `b b1.foo do: cmd` - executes `cmd` when `b1.foo` is called but doesn't stop
- `b b1.foo pre: cmd` - executes `cmd` when `b1.foo` is called and stops


### `catch`

```ruby

class FooException < StandardError; end
class BarException < StandardError; end

def raise_foo
  raise FooException
end

def raise_bar
  raise BarException
end


raise_foo
raise_bar
```

- `catch StandardError` - stops when any instance of `StandardError` is raised, including `FooException` and `BarException`
- `catch FooException` - stops when `FooException` is raised

### `backtrace` (alias `bt`)

**Example Output**:

```
=>#0    Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
  #1    block {|ten=10|} in second_call at target.rb:8
  #2    Foo#third_call_with_block(block=#<Proc:0x00007f9283101568 target.rb:7>) at target.rb:15
  #3    Foo#second_call(num=20) at target.rb:7
  #4    Foo#first_call at target.rb:3
  #5    <main> at target.rb:23
```

- `bt` - shows all frames on the stack
- `bt 10` - only shows the first 10 frames
- `bt /my_lib/` - only shows the frames with path that matches `my_lib`

### `outline` (alias `ls`)

Similar to the `ls` command in `irb` or `pry`.

## `binding.b` + Command Combinations

### `binding.b(do: "b Foo#bar do: bt")`

It allows you to inspect a method call's backtrace without touching the method definition or typing commands manually.

**Script**:

```ruby
binding.b(do: "b Foo#bar do: bt")

class Foo
  def bar
  end
end

def some_method
  Foo.new.bar
end

some_method
```

**Output**:

```
DEBUGGER: Session start (pid: 75555)
[1, 10] in target.rb
=>    1| binding.b(do: "b Foo#bar do: bt")
      2|
      3| class Foo
      4|   def bar
      5|   end
      6| end
      7|
      8| def some_method
      9|   Foo.new.bar
     10| end
=>#0    <main> at target.rb:1
(rdbg:binding.break) b Foo#bar do: bt
uninitialized constant Foo
#0  BP - Method (pending)  Foo#bar do: bt
DEBUGGER:  BP - Method  Foo#bar at target.rb:4 do: bt is activated.
[1, 10] in target.rb
      1| binding.b(do: "b Foo#bar do: bt")
      2|
      3| class Foo
=>    4|   def bar
      5|   end
      6| end
      7|
      8| def some_method
      9|   Foo.new.bar
     10| end
=>#0    Foo#bar at target.rb:4
  #1    Object#some_method at target.rb:9
  # and 1 frames (use `bt' command for all frames)

Stop by #0  BP - Method  Foo#bar at target.rb:4 do: bt
(rdbg:break) bt
=>#0    Foo#bar at target.rb:4
  #1    Object#some_method at target.rb:9
  #2    <main> at target.rb:12
```


### `binding.b(do: "b Foo#bar do: info")`

It allows you to inspect a method's environment (e.g. argument) when called:

**Script**:

```ruby
binding.b(do: "b Foo#bar do: info")

class Foo
  def bar(a)
    a 
  end
end

def some_method
  Foo.new.bar(10)
end

some_method
```

**Output**:

```
DEBUGGER: Session start (pid: 75924)
[1, 10] in target.rb
=>    1| binding.b(do: "b Foo#bar do: info")
      2|
      3| class Foo
      4|   def bar(a)
      5|     a
      6|   end
      7| end
      8|
      9| def some_method
     10|   Foo.new.bar(10)
=>#0    <main> at target.rb:1
(rdbg:binding.break) b Foo#bar do: info
uninitialized constant Foo
#0  BP - Method (pending)  Foo#bar do: info
DEBUGGER:  BP - Method  Foo#bar at target.rb:4 do: info is activated.
[1, 10] in target.rb
      1| binding.b(do: "b Foo#bar do: info")
      2|
      3| class Foo
      4|   def bar(a)
=>    5|     a
      6|   end
      7| end
      8|
      9| def some_method
     10|   Foo.new.bar(10)
=>#0    Foo#bar(a=10) at target.rb:5
  #1    Object#some_method at target.rb:10
  # and 1 frames (use `bt' command for all frames)

Stop by #0  BP - Method  Foo#bar at target.rb:4 do: info
(rdbg:break) info
%self = #<Foo:0x00007fdac491c200>
a = 10
```

I'm a Rails developer, so I usually put the combination code at the beginning of a controller action, like:

```ruby
class SomeController < ApplicationController
  def index
    binding.b(pre: "b User#buggy_method do: info")
    # other code
  end
end
```

And then the debugger would execute the command and/or stops at the method I expected. 
I don't need to jump between multiple files for adding `binding.pry` or `puts` anymore 😎

### ~~A Small Drawback~~

However, the new debugger isn't all perfect (yet). Unlike in `byebug` or `pry`, you can't directly evaluate a Ruby expression in the debug session:

```
(rdbg) 1 + 1
unknown command: 1 + 1
```

To evaluate an expression, you need to use `p` or `pp` command:

```
(rdbg) p 1 + 1
=> 2
```

But according to the project's maintainer [@ko1](https://github.com/ko1)'s '[comment](https://github.com/ruby/debug/issues/148#issuecomment-887687532), expression evaluation may be supported before the official `1.0` release.

#### Update

With https://github.com/ruby/debug/pull/227 being merged, this problem doesn't exist anymore 😉

### Final Thoughts

Although it's not officially released yet, I've started using it at work daily. And I believe it'll soon become an must-have tool in every Rubyists' toolbox. So if you're curious about its capability, I encourage to give it a try 😉