by Jack Kelly
with valued contributions from Alyce Watson, John Fisher, and Anatoly
April 2018
It is rare that a programmer writes any significant amount of code that is entirely free of errors. Often the error is very small, perhaps just one incorrect character. Or perhaps a variable that should be global. Or it could be a major error in logic. All are difficult to see and correct when immersed in the writing and reviewing of code.
There are two categories of programming errors:
Syntax errors – These are grammatical errors that are detected, highlighted, and reported when the program is compiled. They are the programming equivalent of writing a sentence with incorrect spelling, grammar, or punctuation. For example: primt “Hello World! or x < = y
Semantic errors – These are errors in usage or meaning that are only detected when the program is run. They are the equivalent of writing a sentence that is grammatically correct but makes no sense to the reader. Sometimes semantic mistakes cause a system error which halts the program with a message of some sort. For example: x=y/0 or x=sqr(-4)
However, most semantic errors cause only an unexpected, undesirable result in the program output or behavior. These are the errors that are most difficult to find and correct. These are the errors that programmers really mean when they refer to a “bug”.
To minimize the problems caused by programming errors we need good planning, development, testing, and debugging techniques. Many of these good techniques are presented in Alyce Watson’s article in the Liberty BASIC Programming Encyclopedia, Debug Your Code . You should read that article first. Understand and experiment with her suggestions. The remainder of this article reinforces Alyce’s good ideas and hopefully take them a few steps further based on hard-learned, personal experience.
Planning
Any successful creative endeavor, be it programming, writing, musical composition, painting, or drawing, requires planning. You wouldn't expect a contractor to construct a building without a plan, would you? An airplane pilot never leaves the ground without knowing exactly where he or she is going. The overwhelming consensus is that good planning is the best foundation for good programming. Have a clear idea of what you want to do and how you intend to do it BEFORE you start coding. The more detail the better, and in writing. A plan can only help a good thing to be even better.
I've heard it said, “But planning stifles my creativity. I'm much better when I improvise.” I think this attitude is really laziness at worst or fallacious at best. I would reply, “If you don't know or care where you're going, then any road will get you there.” No one would dispute that it's more enjoyable to code than to plan, but that's a poor reason not to plan. Ideas are certainly bound to evolve during development, but that's no reason not to begin with a plan.
You can spend time planning or spend the time modifying and debugging. The choice is yours.
Development
Add code to your program in small modules – no more than, say, 20 to 30 lines at a time. After adding a small module, thoroughly test ALL existing and added functionality with a carefully designed, written, updated procedure.
Something I don't hear much about any more are 'hooks'. A hook is a place holder in your code for a possible future modification. It can be a comment, a variable, an array – anything really. The purpose of a hook is to make it easier to add a planned enhancement without having to analyze and relearn the whole program weeks or months later.
Testing
Here’s a testing tip I learned from a programmer I knew many years ago, Jim Boyland. If a routine does a series of similar, repetitive steps, if the first and last work correctly then those steps in between usually do too.
Debugging
Finding the cause of errors and correcting them is called “debugging” and is the focus of this article. If you observe an error during testing, the cause will probably be in the small, new module that you just added. But it is also possible that code in the new module, or a revised testing procedure, has uncovered a previously undetected error in existing code. The challenge is to find the cause of the error as quickly and efficiently as possible, then correct it. This is the skill and the art of debugging. It is a logical process. You must first find the cause of the error. Then and only then can you correct it. Most emphatically, debugging is not haphazard guesswork. You cannot hope to make corrections that luckily hit the cause of an error and magically make the problem go away. This approach is likely to only cause more problems.
Debugging is like being a doctor. Consider this fictitious scenario. A patient comes to you with a skin rash. You prescribe a medicated cream to put on the rash. Two weeks later the patient comes back with skin rash over an even larger area. You order a blood test which shows too much iron in the blood. You prescribe a drug and a low-iron diet. Two weeks later the patient comes back with skin rash completely covering all of his body. This time you consult your symptom cross-reference database and find that a liver infection can cause both excessive iron in the blood and skin rash. You prescribe penicillin which stops the liver infection, lowers the iron level in the blood, and cures the skin rash.
Computer programmers, doctors, and other professionals such as automobile mechanics, can be prone to guessing and treating the symptoms, the observable manifestations of the problem, instead.
Consistently reproduce the problem
Before beginning the debugging process you must first reduce and simplify the test case to the minimum possible keystrokes that consistently reproduce the problem. If you can’t consistently reproduce the problem with a short, simple testing procedure, it is unlikely that you will be able to find the cause.
Set a breakpoint
Once you can consistently reproduce the problem you can begin to zero in on the code line that causes it. This is done by setting a breakpoint in the program. A breakpoint is a temporary aid which makes the program stop for inspection. A breakpoint can be set in several different ways. One way is to insert a ‘wait’ statement into the program code at an appropriate place.
“Where should I put the breakpoint?” you might reasonably ask. The idea is to set it just before the code line that is causing the error. If you have added a small code module and found an error, the logical place for the initial breakpoint is before the first line of the new module.
Now you can run the program again, enter the test case, and see if you get the error before the program stops at the breakpoint. If you don’t get the error, then move the breakpoint two or three lines down in the new module. When you run the program again, perhaps now you DO get the error before the program stops at the breakpoint. Now you know that one of the code lines between the last two runs is causing the error – or at least the observable manifestation of the error.
Look at the values in the variables
When you know which line is causing the error, replace the ‘wait’ statement with a ‘trace 2’ statement just before the suspect line. Now, instead of running the program normally, you run the Debugger. The Debugger will stop at the ‘trace 2’ statement and display a list of all the variables the program has referenced up to that point. You can easily see the values of the variables the suspect code line will use when it executes. From here you should be able to pinpoint the ultimate cause of the error. It may be that the code which assigns a variable used by the suspect line is the actual cause of the error.
When the cause of the error is found, do NOT make ANY code change that does not directly correct the problem. It is important not to introduce another error while attempting to correct one error. If you make a change that does not fix the problem – change it back! If during debugging you find other code in your program that should be modified, make a written note of it for future correction. Do not change it while looking for the problem at hand.
Every program bug is unique
The previous scenario is an ideal oversimplification. This was a case when the error caused something observable to happen that was not expected. The opposite can also occur. The error could cause something NOT to happen that was expected. In this case you still need to set a breakpoint and look at the variables. But now you have to analyze the program logic to determine which line of code was supposed to make the action happen, and set the breakpoint there. Hopefully it is in the small module that you just added.
Every program and every error is unique. There is no way to reduce the debugging process to a mechanical checklist. There are things we can do that are sometimes effective, but the debugging process is completely subjective. It depends on knowledge, skill, logic, observation, and intuition. Don’t be disheartened – it IS possible to debug, and even to enjoy it. Remember what they say, “If you truly like your job, you also like its mundane and difficult aspects too.”
The Debugger is a program that runs your program. It was designed and coded by world-class software engineers solely for your benefit. To run your program using the Debugger click the small bug button on the IDE toolbar, rather than the blue > button next to it that you usually use. The Debugger is fully described in the Help system. Search for ‘debugger’ in the Help topics window. Read the instructions, experiment with it, and try to understand it as well as possible before you try to use it to find a real program error. But, by all means, try to use it.
It is possible that you may find the Debugger to be a bit too much. The Debugger window lists ALL the variables in use by the program even though you are usually only interested in one or two of them for a particular error. And the Debugger window always seems to be in the way. It allows you to single step through your program, but I have never found this to be of much help.
With a little more effort you can set breakpoints and use them without the Debugger. As mentioned previously, the ‘wait’ statement can be used as a breakpoint. In conjunction with a remark, it can easily be found again in a large program by using Search. For example: wait ’xxx. A downside is that it must be deleted or ‘rem’ed before the program can be run normally. Debugger breakpoints (‘Trace 2’ and left-margin, clickable breakpoints) are ignored when the program is run normally. The latter are not saved, though.
With a little more effort you can also view the value of variables without the Debugger. Most of the time you will be correcting errors in GUI programs. You can make a temporary statictext control in an unused part of the program window, perhaps at the very top or bottom, and apply a small font to it. You can then print the content of one or more variables to it and be able to view them with no additional overhead.
For example:
statictext #win.debug, "Debug Line", 0, 0, 785, 17 #win.debug "!Font Arial 10" #win.debug ”a$="; a$; ” n=”; n wait ’xxx
If you need to see more diagnostic information than can fit on the statictext control, you can ‘print’ to the MainWin. For example array variables, which are not listed in the Debugger. If you need to see the contents of an array, you could put a temporary ‘for-next’ loop with a print statement before the breakpoint. When the program stops the array values will be listed in the MainWin. To do this you must ‘rem’ the ‘NoMainWin’ statement, and remember to un‘rem’ it before running the program normally.
You might find that a line in a loop is possibly causing the error. Your program might loop through this line any number of times but analysis tells you that only the third iteration is problematic. You can set a conditional 'trace 2' or 'wait' breakpoint that will only stop the program on the third pass through the loop. For example: if n=3 then wait
Left margin, clickable breakpoints can only be unconditional in the Debugger.
If you've spent many hours and haven't been able to find the cause of an error or to correct it, then STOP. Take a break, do something else to get your mind off it, or get a good night's sleep. When you go back to the problem it's not unusual for the answer to jump right out at you. It happens all the time.
You have written a short program and it is working well.
NoMainWin WindowWidth = 800 WindowHeight = 600 statictext #win.title, "Counting Program", 240, 80, 305, 40 statictext #win.display, "", 315, 150, 103, 60 statictext #win.message, "Click Start to Begin", 240, 220, 200, 25 button #win.start, "Start", [StartClick], ul, 210, 280, 100, 30 'button #win.pause, "Pause", [PauseClick], ul, 420, 280, 100, 30 open "Counting Program" for window_nf as #win #win "TrapClose Quit" #win "Font Arial 10" #win.title "!Font Arial 14 Bold" #win.display "!Font Arial 20 Bold" wait [StartClick] #win.message "" [LoopStart] if Count<10 then Count=Count+1 else #win.message "Counting complete." wait end if #win.display Count timer 1000, [delay] wait [delay] goto [LoopStart] '[PauseClick] 'wait sub Quit CallingHandle$ close #win end end sub
However, you decide it needs a small modification. You want to add a 'Pause' button which will stop the counting. Clicking the 'Start' button will resume the counting where it left off. I've made it easy for you to add your modification. Just un'rem' three lines – one for the button, and two for its click routine.
Not surprisingly, the modification doesn't work. It's always a pleasant surprise when a modification, even a small one, works the first time. But more often than not that is not the case. Why doesn't the 'Pause' button stop the counting? Is something wrong with the code you just added, or was something already wrong before? If you find the cause and correction, don't give the answer away. Everyone has to work for it.
This is an excellent series of articles by Alfred Thompson, a computer science educator. The articles are based on a book, 'Programming Proverbs', by Henry Ledgard. Each article discusses one of the 26 proverbs, followed by comments from various readers. It's clear to see from the comments the wide range of opinions that surround program planning, development, testing, and debugging.
This is an article by a very high-powered programming professional, Eric Lippert. He worked at Microsoft for many years. Although he seems a bit arrogant and cynical, one can't dispute his good advice.