What can Byebug do for your debugging?

Matthew Kloster
GitClear
Published in
6 min readOct 10, 2017

Byebug is an all-in-one debugging utility for Ruby. It lets you:

1. Stop execution anywhere in any piece of code to look around and see what’s going on

2. View a complete backtrace of every bit of code leading up to where you are (including any framework code)

3. Navigate around, step into, and continue through any additional code calls.

The benefits are immediate and immense. Without the advantage of a massively comprehensive test suite, you’ve probably, at least once, said “What the heck is going on?” when you observe how a piece of code behaves. Byebug helps to solve that mystery.

There are several ways to use byebug, but we’ll demonstrate the simplest and most common case: Dropping a byebug somewhere within your lines of code to see what’s going on.

Let’s say we’re testing a new factorial function, with an example test case of fac(4)printing out 12. Something is obviously amiss. Let’s look at what code we’ve set up to do this (hopefully you’ll be able to spot the bug here pretty quickly):

def fac(num)
if num <= 2
1
else
num * fac(num - 1)
end
end

puts(fac(4))

Instead of relying on our ability to manually read this out and come to a conclusion about what the bug is, let’s plop a byebug at the top of this function and see what we get at each step…

require "byebug"def fac(num)
byebug
if num <= 2
1
else
num * fac(num - 1)
end
end

puts(fac(4))

Once we get the program running, we’ll see what’s going on under the hood. First by checking what num is (to make sure that we aren’t seeing any funky values) and then checking the backtrace of where the breakpoint is:

matthewk@matthewk-bonanza ~ $ ruby fac_example.rb[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
=> 5: if num <= 2
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug) num
4
(byebug) backtrace
--> #0 Object.fac(num#Integer) at /home/matthewk/fac_example.rb:5
#1 <main> at /home/matthewk/fac_example.rb:12

Nothing sticks out so far. We’ll hit next to navigate further in the code, and then eventually step to see what’s going on when we step into that recursive fac call…

[3, 12] in /home/matthewk/fac_example.rb
3: def fac(num)
4: byebug
5: if num <= 2
6: 1
7: else
=> 8: num * fac(num - 1)
9: end
10: end
11:
12: puts(fac(4))
(byebug) step
[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
=> 4: byebug
5: if num <= 2
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug) num
3

Still looking okay, and the number goes down by one, as we’d expect… so let’s keep going.

(byebug) next[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
=> 5: if num <= 2
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug) next
[3, 12] in /home/matthewk/fac_example.rb
3: def fac(num)
4: byebug
5: if num <= 2
6: 1
7: else
=> 8: num * fac(num - 1)
9: end
10: end
11:
12: puts(fac(4))
(byebug) step
[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
=> 4: byebug
5: if num <= 2
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end

It’s still looking good. Let’s just verify that we get the number (2) that we expect, and that the backtrace looks OK.

(byebug) num
2
(byebug) backtrace
--> #0 Object.fac(num#Integer) at /home/matthewk/fac_example.rb:4
#1 Object.fac(num#Integer) at /home/matthewk/fac_example.rb:8
#2 Object.fac(num#Integer) at /home/matthewk/fac_example.rb:8
#3 <main> at /home/matthewk/fac_example.rb:12

Looks like the number is correct and we have all of our recursive calls. Let’s continue:

(byebug) next[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
=> 5: if num <= 2
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug) next
[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
5: if num <= 2
=> 6: 1
7: else
8: num * fac(num - 1)
9: end
10: end

Wait a minute.

At this point, you should notice that heading into this branch (returning 1 when the number is 2) seems faulty. When we peer into that if statement, we observe that the number 2 isn’t the correct number to place there — it’s 1. So we replace it with 1, rerun the program, and all is well.

One neat thing that byebug offers us is the fact that hitting the return key repeats the last command entered. For example, we could be using our next command repeatedly to see what our (now fixed) result gets used as. Say that we update our byebugged code to be this:

require "byebug"def fac(num)
byebug
if num <= 1
1
else
num * fac(num - 1)
end
end

fac_4 = fac(4)
if fac_4 == 24
puts("It worked!")
else
puts("It's broken!")
end

We can then step through the call and observe which `puts` line it gets to via this (note the blank lines, then the `continue` at the end, which moves to the end of the execution):

matthewk@matthewk-bonanza ~ $ ruby fac_example.rb[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
=> 5: if num <= 1
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug) next
[3, 12] in /home/matthewk/fac_example.rb
3: def fac(num)
4: byebug
5: if num <= 1
6: 1
7: else
=> 8: num * fac(num - 1)
9: end
10: end
11:
12: fac_4 = fac(4)
(byebug)
[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
=> 5: if num <= 1
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug)
[3, 12] in /home/matthewk/fac_example.rb
3: def fac(num)
4: byebug
5: if num <= 1
6: 1
7: else
=> 8: num * fac(num - 1)
9: end
10: end
11:
12: fac_4 = fac(4)
(byebug)
[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
=> 5: if num <= 1
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug)
[3, 12] in /home/matthewk/fac_example.rb
3: def fac(num)
4: byebug
5: if num <= 1
6: 1
7: else
=> 8: num * fac(num - 1)
9: end
10: end
11:
12: fac_4 = fac(4)
(byebug)
[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
=> 5: if num <= 1
6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug)
[1, 10] in /home/matthewk/fac_example.rb
1: require "byebug"
2:
3: def fac(num)
4: byebug
5: if num <= 1
=> 6: 1
7: else
8: num * fac(num - 1)
9: end
10: end
(byebug)
[9, 18] in /home/matthewk/fac_example.rb
9: end
10: end
11:
12: fac_4 = fac(4)
13:
=> 14: if fac_4 == 24
15: puts("It worked!")
16: else
17: puts("It's broken!")
18: end
(byebug)
[9, 18] in /home/matthewk/fac_example.rb
9: end
10: end
11:
12: fac_4 = fac(4)
13:
14: if fac_4 == 24
=> 15: puts("It worked!")
16: else
17: puts("It's broken!")
18: end
(byebug) continue
It worked!

We can also modify variables as we execute our program. In this example, we print out a set of factorials with our now-working program:

require "byebug"def fac(num)
if num <= 1
1
else
num * fac(num - 1)
end
end

numbers = [ 3, 4, 8, 11 ]
byebugputs( numbers.map { |number| fac(number) } )

Let’s run it and then change numbers to a simpler array, then see what we come up with:

matthewk@matthewk-bonanza ~ $ ruby fac_example.rb[6, 15] in /home/matthewk/fac_example.rb
6: else
7: num * fac(num - 1)
8: end
9: end
10:
11: numbers = [ 3, 4, 8, 11 ]
12:
13: byebug
14:
=> 15: puts( numbers.map { |number| fac(number) } )
(byebug) numbers = [ 1, 2, 3, 4 ]
[1, 2, 3, 4]
(byebug) continue
1
2
6
24

In fact, you can run literally any command you’d use at any point of your script. This makes it quite possible to observe what would happen if you added different variations of a piece of code after a byebug breakpoint.

This is a great way to get you going with byebug and use it in a way that helps you find and squash bugs quickly in your application. Interested in going into more detail? See the official docs. If you’re interested in a comparison inline debugger, you can check out pry. Note, however, that if you’re developing for Rails, byebug is considered the default, well-supported debugger so you’ll probably get a lot more support for debugging within byebug than pry in that setting.

If you’re interested in getting even more out of your code, at GitClear, we make the process of understanding changes in the code and measuring productivity incredibly easy. One click gets you a full analysis of each change in your repository instantly available. We support 6 languages (including Ruby) with more on the way.

--

--

Matthew Kloster
GitClear
Writer for

A human being who likes coding, beer, and the company of other human beings.