Teaching Novice Programmers How to Debug Their Code
The most effective debugging tool is still careful thought, coupled with judiciously placed print statements. Brian Kernighan, "Unix for Beginners" (1979)
Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it? Brian Kernighan, "The Elements of Programming Style", 2nd edition, chapter 2
Of all the skills we teach at CodeUnion, we focus most relentlessly on teaching our students how to debug effectively. Why do we think this skill is so critical and, more importantly, how does one teach a novice how to debug?
Debugging - The Critical Programming Skill
One of the biggest surprises for novice programmers is just how much of their "programming time" is spent as "debugging time." They often feel that this time is wasted or that it's a kind of punishment for them not being smart enough to write the correct code the first time through. "If I knew what I was doing," they think to themselves, "I wouldn't have to spend so much time debugging."
It's only after they've written their fair share of code that they realize every programmer spends a large amount of time debugging. If novices are going to be spending much of their time debugging then shouldn't we, as teachers, be spending a proportional amount of time teaching them how to do it well?
Beyond that, you, as a teacher, want your students to do more than learn whatever particular subject you're teaching — you want them to carry that subject with them throughout their lives and thrive in future situations where it's relevant. Unfortunately, you face a few harsh realities:
- Most of your students' learning will happen long after you were "officially" their teacher.
- Even while you're "officially" their teacher, most of their learning won't happen in your presence or under your direct guidance.
- It's impossible to prepare your students for every possible future situation, even assuming you knew them all.
This means that often the best thing you could do for your students' long-term success is not to teach them the particular details of your subject, but teach them how to orient themselves in uncertain or confusing situations. In the context of learning how to program, what is debugging if not the art of making sense of an uncertain or confusing (albeit programming-related) situation?
Debugging — Experts vs. Novices
As experienced programmers looking to teach novices how to debug code, there are two things to always keep in mind:
- Few novice programmers think about debugging the way you do.
- Even fewer have the good debugging habits you exercise without thinking.
Our job, then, is to minimize our students' bad habits while helping them develop more effective ones. To do this, we need to identify the most important habits present in experienced programmers but absent in novices, understand why novices lack those habits, and illustrate what we — more experienced programmers — would have done differently and why.
Here are two common "missing habits" that we try to develop at CodeUnion.
Establishing A Shared Context
In most situations, we don't go into "debugging mode" until our program does something that surprises us, e.g., crashes and prints out an error message. When this happens to a student, they'll often approach us with little more than "My code isn't doing what I want. What's wrong with it?"
A more experienced programmer knows that this isn't an effective way to ask for help. They're thinking to themselves, "How is the person I'm asking supposed to know anything about my code, what I intended for it to do, or why I believe it's broken? Of course I need to share more context." Because they know this, the experienced programmer will habitually phrase their question along the lines of "I wanted to do X, so I wrote <some-code>
. I expected to see Y when I ran this code, but instead I saw Z. What am I missing?"
This interaction happens all the time at CodeUnion. Here's a somewhat-idealized version:
Student: My code isn't working. Can you tell me what's wrong with it?
Teacher: I'm not psychic! I need more information. :)
Can you answer these questions as best as you can, first?
1. What makes you say your code isn't working?
2. What did you expect your code to do and why?
3. What did your code do instead and how do you know?
Student: <answers questions>
Ohhh, I see what's wrong!
Teacher: Imagine if you had a habit of asking and answering these
questions yourself. Half the time you'd solve your own
problem and the other half you'd be able to ask a much
more specific question and get relevant help more quickly.
Student: I totally agree.
Why don't novices do this out of habit? In most cases, it's because they believe that no matter the problem, an experienced programmer "must" have the solution. After all, isn't that what "experienced" means? This is corroborated by the fact that they've witnessed you solve in minutes a problem that they just struggled with for hours.
Think about that for a second the next time a novice approaches you out of the blue with a frustrating question like "What's wrong with my code?" They're probably doing it because they're convinced you have God-like insight into every programming problem that could come their way. They don't realize that you can solve a wide range of problems so quickly because you're good at debugging and that asking and answering those questions for ones self is the key first step to the process.
Listing And Testing (Hidden) Assumptions
When our code isn't behaving as expected then one of our assumptions must be wrong. If only we could list our assumptions precisely enough to test them one by one, we'd be set. Unfortunately, most bugs are caused by the assumptions we didn't realize we were making, ranging from the forehead-slapping (e.g., "My code is free of typos") to the more fundamental (e.g., "I understand how this-or-that component works and I'm using it correctly.")
That means the most effective debuggers have to quickly surface and test hidden assumptions. Very few students have experience being this precise or realizing when they're making a hidden assumption. As teachers, however, we can help by listing their assumptions for them precisely enough that they can make forward progress. This allows students to see what assumptions are important and why and gives them an example of how to approach this on their own.
Here's an actual chat transcript from the CodeUnion Slack group chat. The student's name has been changed to protect their privacy. Warning: it's a little long, but we wanted to include an actual transcript.
For context, this student was building a simple database-backed web application in our Fundamentals of Web Development workshop that contained a messages
table and a corresponding Message
model. They were trying to create a new Message
record, but it wasn't being saved to the database because they weren't providing the required information. On the one hand, we could point out what required information the student was omitting. On the other hand, we could demonstrate how we, as more experienced programmers, might figure this out on our own.
Spoiler alert: The student concludes with "Cool. I am reprogramming the way I address problems."
Aaron: I'm trying to add code that allows a user to create a new message.
Aaron: I created a `Message` class for this purpose with its own
properties
Aaron: For some reason it's not saving. I know this because I have codes
that states...
post('/messages') do
@message = Message.create(message_params)
if @message.saved?
redirect('/')
else
render('new_message')
end
end
Aaron: and I'm rendering `new_message`
Jesse: So, in that code, which line is not doing what you expect?
Aaron: @message = Message.create(message_attributes)
Jesse: Ok. What are you expecting it to do?
Aaron: I'm expecting it to create a new record containing the message
parameters
Jesse: How do you know it's not doing that?
Aaron: I was expecting a redirect to one page and it rendered the other
Jesse: Ok, that's true, but I'd be more granular. I'd say:
Jesse: "If the record had been saved, `@message.saved?` would have
returned `true`, but instead it returned `false`."
Aaron: ah... granularity
Jesse: That's the _specific_ part of the code that's telling you whether
the record was saved or not.
Jesse: Here's what we know from the code alone (without looking at
anything else, including the form):
1. There's an incoming request with various parameters
2. We're calling `Message.create` with `message_params` as an
argument
3. Hence, whatever `message_params` is, it must contain all the
information that `Message.create` expects (in the format that
`Message.create` expects).
4. We expect `@message.saved?` to return `true`, but instead it
returned `false`
Aaron: this is accurate
Jesse: *IF* `message_params` contains everything that `Message.create`
needs to do its job *THEN* `@message.saved?` should return `true`.
Jesse: Would you agree with that statement?
Aaron: i hadn't thought about it like that before now, but I agree.
Jesse: However, `@message.saved?` is returning `false`
Aaron: so, `message_params` must not contain everything that
`Message.create` needs
Jesse: Yes!
Jesse: If all of our assumptions (1) - (4) are correct then we've proven
that `message_params` doesn't contain everything that `Message.
create` expects.
Jesse: So _either_ one of our assumptions is wrong (always possible) or
`message_params` doesn't contain everything that `Message.create`
expects.
Aaron: then I have to ask myself what does Message.create expect?
Jesse: Yes, 100%
Jesse: And I can say, as a teacher
Jesse: The odds of you knowing that off-hand what `Message.create` expects
is probably pretty small. :grinning:
Jesse: Rather, this is an exercise in getting you to notice that that's
the thing to pay attention to, here.
Aaron: but the odds of GOOGLE knowing that...
Jesse: Bingo.
Aaron: Ok, the first thing I'm going to do is see what's in
`message_params` and see whether it's missing anything I expect.
Jesse: Great idea. And if it contains everything you expect, then you
know there's something that `Message.create` expects that you're
unaware of.
Aaron: cool. i am reprogramming the way i address problems.
Debugging Is A Learned Skill
Every programmer spends a large amount of their time debugging code, whether it's their own or someone else's. Beginners, students, and other novices spend even more of their time struggling with code that doesn't do what they expect, almost by definition. Teaching them how to debug their own code effectively is the single most valuable skill we could teach — it's the skill that makes acquiring all other programming skills easier.
The habits we described above — establishing a shared context and surfacing hidden assumptions — are just two of maybe a dozen or so that experts rely on every day. As teachers, helping students develop and internalize these habits early on will pay dividends throughout their careers (and the time spent as our students). We'd be well-served to emphasize it from the start.
If you want more writing on this topic, shoot us a note on Twitter, Facebook, or via email at [email protected].