A hands-on tutorial to debugging your code with pry-byebug
Like most new developers, I first started as a puts
debugger. I would write puts
everywhere to see what’s what.
Debugging is hard enough, so knowing your tools makes that task much simpler. When I learned the ropes of pry-byebug, I decided to passed that knowledge along to anyone who’d hear it!
Let’s dive right in, so I can give you practical tips on using pry-byebug in your Rails applications.
Set up pry-byebug
Add the gem to your gemfile.
Then run:
That’s it.
Some context
Before we begin, let’s build up a bit of context, so my demo is more idiomatic. I’ll draw on something that happened to me a while back.
Here’s our scenario:
- Some third-party service periodically sends us a webhook with people’s information.
- We receive and handle the data: checking for people in our database, creating them if need be, etc.
- We check that people were either matched to an existing instance or created.
Sounds good? Let’s check our code now.
First, here’s an excerpt from the data we expect to receive:
One important thing to note: this is what we expect. At that point, we only know for sure about the structure of the data. The data itself? We don’t know yet what we’ll receive.
Second, here’s our route.
Third, here’s our controller code:
What does it do:
- Loops over each person contained in the params.
- Finds or creates each person.
- Assigns the ad hoc values and persists them.
- Handles unexpected errors.
Remember when I told I’d drawn the context from my own experience? Well, here’s what happened:
After a while, my users reported some discrepancies in the app: some people sent by the third-party service were neither found nor created. No clear pattern emerged from the get-go. 🤔
‘Twas time for some binding.pry
.
pry-byebug basic commands
Add a breakpoint and check current values: binding.pry
Let’s go back to our SomeServiceHooksController
. I’ll add two breakpoints inside the loop. It’ll help me check the data at different stages.
Now, I can send the concatenated content of the last few webhooks to our controller (with Postman, for instance). Each binding.pry
will pause the execution of our code. And, right amid our server’s logs, pry-byebug will open a debugging console.
Let me show you what happens after I send the data to the endpoint:
Behold pry-byebug’s console!
See that =>
in front of line 52? That’s pry-byebug telling you the execution paused there.
From here, you can check every variable already declared in the present context: our params
and the current person
.
Typing params
in the console will give you the following output:
Typing person
will give you:
But soon-to-be-defined variables - like new_person
- are not accessible yet.
I can also call the methods defined in my class because Ruby reads class definitions before their execution. I can even query things from my database:
Fancy right? Let’s move on.
Execute the next line of code and wait: next
So, our application paused just before new_person
’s definition. What if we want to move the cursor down one line and see what Person.find_or_create_by(email: person[:email])
returns?
Well, we can type next
in our pry-byebug console.
This’ll output:
See what happened? The =>
cursor moved through the definition of new_person
, and stopped just before the next line of code. So now, new_person
is accessible:
Tada! An existing instance of Person
was affected to new_person
because it existed with the email buffy@sunnydale.edu
.
Continue execution until the next breakpoint (or until the end of the current process): continue
Now, remember when I said I’d throw a couple of breakpoints for good measure? Sometimes, I want to skip big chunks of code but still pause its execution later. Think about controllers calling multiple methods, one of which is faulty. I can add breakpoint in each method to see what’s what.
In this case, next
is not enough. You’d have to type it countless times and (sometimes) navigate through your app’s stack (something that’s way too much advanced for this tutorial).
So, what should you use? continue
of course! Lemme show you:
And the output:
The =>
cursor didn’t just move onto the next line. It only stopped at the next breakpoint. So now, I have access to the updated version of new_person
:
Once you’ve passed the last breakpoint, continue
will resume the code’s execution until the end of the current context. In our example, the current context is SomeServiceHooksController
. Since we’re in a loop, continue
will only bring you to the next person
until we looped through every people. Then, it’ll exit the SomeServiceHooksController
class and get on with its life.
Can’t remember where your breakpoint is? Use whereami
This one I learned recently. Pretty useful when you’re checking a lot of values and your current context has gone up beyond your reach. Output your breakpoint’s position with whereami
. Neat!
Exit pry-byebug
Want to exit pry-byebug the dirty (my) way? Try exit!
. It’ll kill both the current process and the server. This is useful when having too many breakpoints across multiple places.
⚠️ A warning: since binding.pry
’s pause code execution, don’t push ‘em into production. Or you’ll be in for a rough time. 😬
How my debugging ended up: never trust your database
When I learned to code, a lot of people told me to never trust users’ input. Well, no one ever told me to never trust my database either!
Remember when I told you that I was only sure about the structure of the data sent through the webhook? Well, it turned out that some people’s data were incomplete:
Lesson #1: never trust your users’ input
You would expect Person.find_or_create_by(email: nil)
to return nil
, right? Well, it turned out some of my oldest instances of Person
had been created before I had a validation of presence in place for their email.
So, every time the third-party service would sent me a person with a nil
email, I would update the first instance of Person
with an email == nil
instead of creating a new one (or handling this as an error). These people fell from the net for weeks before I managed to identified the core problem.
Lesson #2: never trust your own data
Well, that’s it for today folks! I hope it’ll make your debugging more enjoyable!
Noticed something? Ping me on Twitter or create an issue on GitHub.
Cheers,
Rémi
P.S.: This post was featured in Ruby Weekly’s 496 edition.