Software Developer and GNU/Linux specialist

Javascript debugging: breakpoints versus print statements

Conventional wisdom holds that print statements are poor practice for debugging code. Proponents of this view, myself included, reason that debugging code should be a nondestructive process. One should not change the code to diagnose it, only to fix it. Under normal deterministic conditions, I see no reason to say otherwise. Print statements are a poor substitute for line-level debugging and inspection. Excellent facilities for debugging, inspection, and profiling exist in most languages in current use, including JavaScript. If you’re thinking clearly just now, you probably want to ask me something like “fine, so are there circumstances under which you consider print statements a good practice?” There are, and I’ll tell you what I think they are.

Understand this first. I see no reason for print statements under deterministic conditions in which a debugger is available. In other words, I take it as a basic principle that so long as one can access a step debugger and control program inputs, and the program under such conditions exhibits consistent and predictable behavior, there is no good reason to alter the subject code. Any counterexamples of the rule must assume conditions that negate one of the premises of that principle. In the case of JavaScript programs, we have virtually universal access to a debugger. At least one browser-based debugger exists for Node.js (and, by extension, several other frameworks), and most other JS code that you will likely encounter as a developer runs in a browser, so the browser’s debugger provides the necessary facility. If you are using Chrome, you can even manipulate the code during run time and save changes to the file system. As widely used as JavaScript is, the near ubiquity of debuggers could change in the future. In that case you have a counterexample. Otherwise, no dice. And front-end developers can check academic arguments at the door.

All of the above being said, we have some other very good reasons to ignore the rule and use print statements or other code alterations for debugging. JavaScript behavior in browsers is not always deterministic, just mostly so. Timing means a lot in asynchronous systems. Suppose that an entirely synchronous local script depends on a datum from an asynchronous request to a server (the way we do most Web requests now). Prior to the receipt and processing of that datum, the local state may be called A. After the datum is received and a callback function triggered, the local state may be called B. Assume that the local script fails on line x, and I want to debug it. Being the careful and diligent developer that this writing demonstrates I am, I open my dev panel and set a breakpoint on the first executable statement in the local function, at line w (y lines before x, or line x – y). I step through the function until I hit line x. Expecting the failure to repeat (because in a troubleshooting setting I always want to reproduce the error as long as it’s not dangerous or destructive to do so), I step through the statement. It fails to fail as expected; I click the “continue” icon, and the function executes and the page displays as it is designed to. What happened?

Suppose that I am also astute enough to check the network activity panel of the browser. There I see the request in question (I can even ensure that it’s the correct one, because my favorite browser provides a stack trace for each request, though not yet for each websocket exchange). I confirm that it executed in 1.0546 seconds. But it took me several seconds to step from line (x – y) to line x, where the failure occurs under normal conditions. What is the state of the system (at least the relevant component(s) of that state) in that interval? Is it A or is it B? I can be a very fast clicker to find out using the debugger, or I can insert a console.log(…) at line x. Doing the latter causes the browser to spit out a navigable snapshot of a given datum that matches the state of the system at the moment the log statement executed. I can come back to it two days later and, assuming the computer is still running and the browser is still up, I can examine the now obsolete state. I can then make an informed decision on how to handle that state: whether to change it, provide a promise or other waiting mechanism, or something entirely different.

Depending on their content and use, print statements may be one example of what Andrew Hunt calls “tracer bullets” in The Pragmatic Programmer (49). Though not every debugging situation warrants the use of code-altering debug statements, in some cases they provide invaluable access to otherwise unreachable information about the inner state of a program. They should be among the trusted tools of any expert developer.

Post a Comment

Your email is kept private. Required fields are marked *