Content
Debugging is initially twice as difficult to write code. Therefore, if you write code as abstruse as you can, then by definition you are not able to debug it.
Brian Kernigan and PJ Plauer, Basics of Program StyleYuan-Ma wrote a small program that uses many global variables and terrible hacks. The student, reading the program, asked him: “You warned us about similar techniques, but at the same time I find them in your own program. How is this possible? ”The master replied:“ There is no need to run after the watering hose if the house is not on fire. ”
')
Master Yuan-Ma, "Programming Book".A program is a crystallized thought. Sometimes thoughts get confused. Sometimes, when thoughts are turned into a program, mistakes sneak into code. In both cases, the result is a damaged program.
Deficiencies in programs are usually called bugs. These may be programmer errors or problems in the systems with which the program interacts. Some errors are obvious, others are elusive, and may be hidden in systems for years.
Often the problem arises in those situations, the occurrence of which the programmer initially did not foresee. Sometimes these situations cannot be avoided. When a user is asked to enter his age, and he enters an "orange", this puts the program in a difficult situation. These situations need to be foreseen and somehow handled.
Programmer errors
In the case of programmer errors, our goal is clear. We need to find them and fix them. Such errors range from simple typographical errors, which the computer will complain to immediately, as soon as it sees the program, to hidden errors in our understanding of how the program works, which lead to incorrect results in special cases. Errors of the latter kind can be searched for weeks.
Different languages can help you find bugs in different ways. Unfortunately, JavaScript is located at the end of this scale, designated as “hardly helps at all”. Some languages need to know exactly the types of all variables and expressions before running the program, and they will immediately tell you if the types are used incorrectly. JavaScript only considers types during the execution of programs, and even then it allows doing not very meaningful things without any complaints, for example
x = true * ""
Some things JavaScript does complain about. Writing a syntactically incorrect program will immediately cause an error. Other errors, such as calling something that is not a function, or calling the property of an undefined variable, will occur when the program is executed when it encounters such a meaningless situation.
But often your meaningless calculations just spawn NaN (not a number) or undefined. The program will happily continue, being confident that it is doing something meaningful. The error will manifest itself later when such a dummy value has already passed through several functions. It may not cause an error message at all, but simply lead to an incorrect execution result. Finding the source of such problems is a difficult task.
The process of finding bugs in programs is called debugging.
Strict mode
JavaScript can be forced to be stricter by translating it into strict mode. For this, use strict is written at the top of the file or function body. Example:
function canYouSpotTheProblem() { "use strict"; for (counter = 0; counter < 10; counter++) console.log(" "); } canYouSpotTheProblem();
Usually, when you forget to write var before a variable, as in the example before counter, JavaScript quietly creates a global variable and uses it. In strict mode, an error is thrown. It is very convenient. However, an error is not generated when a global variable already exists - only when the assignment creates a new variable.
Another change is that this binding contains undefined in those functions that were not called as methods. When we call a function that is not in strict mode, this refers to the global scope object. Therefore, if you accidentally incorrectly invoke the method in strict mode, JavaScript will generate an error if it tries to read something from this, and does not happily work with the global object.
For example, consider the code that calls the constructor without the new keyword, in which case this will not refer to the object being created.
function Person(name) { this.name = name; } var ferdinand = Person("");
An invalid Person call succeeds, but returns as undefined and creates a global variable name. In strict mode, everything is different:
"use strict"; function Person(name) { this.name = name; }
We are immediately reported an error. Very comfortably.
Strict mode can do something else. It prohibits calling a function with several parameters with the same name, and removes some of the potentially problematic properties of a language (for example, a with statement that is so terrible that it is not even discussed in this book).
In short, “use strict” before the program text rarely causes problems, but it helps you to see them.
Testing
If the language is not going to help us in the search for errors, we have to look for them in a complex way: by running the program and observing whether it does something as it should.
Doing it manually, over and over again is a sure way to go crazy. Fortunately, it is often possible to write another program that automates the checking of your main program.
For example, let’s look at the Vector type again.
function Vector(x, y) { this.x = x; this.y = y; } Vector.prototype.plus = function(other) { return new Vector(this.x + other.x, this.y + other.y); };
We will write a program that checks that our implementation of Vector works as it should. Then, after each change in the implementation, we will run a verification program to make sure that we have not broken anything. When we add functionality (for example, a new method) to the Vector type, we add checks to this new functionality.
function testVector() { var p1 = new Vector(10, 20); var p2 = new Vector(-10, 5); var p3 = p1.plus(p2); if (p1.x !== 10) return ": x property"; if (p1.y !== 20) return " : y property"; if (p2.x !== -10) return " : negative x property"; if (p3.x !== 0) return " : x from plus"; if (p3.y !== 25) return " : y from plus"; return " "; } console.log(testVector());
Writing such checks results in duplicate code. Fortunately, there are software products that help to write sets of checks with the help of a special language adapted for writing checks. They are called the testing frameworks.
Debugging
When you notice a problem in the program - it behaves incorrectly and gives errors, - it's time to find out what the problem is.
Sometimes it's obvious. The error message directs you to a specific program line, and if you read the error description and this line, you can often find a problem.
But not always. Sometimes the line that leads to an error simply turns out to be the first place where the incorrect value obtained elsewhere is used incorrectly. Sometimes there is no error message at all - there is just an incorrect result. If you did the exercises from the previous chapters, you must have fallen into such situations.
The following example tries to convert the number of a given number system to a string, subtracting the last digit and making a division, to get rid of this digit. But the wild result produced by the program hints at the presence of an error in it.
function numberToString(n, base) { var result = "", sign = ""; if (n < 0) { sign = "-"; n = -n; } do { result = String(n % base) + result; n /= base; } while (n > 0); return sign + result; } console.log(numberToString(13, 10));
Even if you find a problem, pretend you haven't found it yet. We know that the program is failing, and we need to know why.
Here you need to overcome the desire to start making random changes to the code. Think instead. Analyze the result and come up with a theory on which it happens. Make additional observations to test the theory — and if there is no theory, make observations that would help you invent it.
Placing multiple console.log calls in strategic locations is a good way to get additional information about what the program is doing. In our case, we need n to take the values 13, 1, then 0. Let's output the values at the beginning of the cycle:
13 1.3 0.13 0.013 … 1.5e-323
Y-yes. The division of 13 by 10 does not yield an integer. Instead of n / = base, we need n = Math.floor (n / base), then the number will be correctly “shifted” to the right.
In addition to console.log, you can use the debugger in the browser. Modern browsers are able to put a breakpoint on the selected line of code. This will pause the program execution each time the selected line is reached, and then you can view the contents of the variables. I will not describe the process in detail, since it is organized differently in different browsers - look for developer tools in your browser. Another way to set a breakpoint is to include in the code an instruction for the debugger consisting of the debugger keyword. If the developer tools are active, the execution of the program will be suspended on this instruction, and you can examine the status of the program.
Error propagation
Unfortunately, the programmer can prevent the appearance of not all problems. If your program communicates with the outside world, it may receive incorrect input data, or the systems with which it tries to interact may be broken or inaccessible.
Simple programs, or programs running under your supervision, can simply “give up” at such a moment. You can study the problem and try again. "Real" applications should not just "fall". Sometimes you have to accept incorrect input data and somehow work with them. In other cases, you need to inform the user that something went wrong - and then give up. In any case, the program should do something in response to a problem.
Suppose you have a function promptInteger, which requests an integer and returns it. What should she do if the user enters an "orange"?
One option is to return a special meaning. Usually for these purposes use null and undefined.
function promptNumber(question) { var result = Number(prompt(question, "")); if (isNaN(result)) return null; else return result; } console.log(promptNumber(" ?"));
This is a reliable strategy. Now, any code calling promptNumber should check if a number was returned, and if not, somehow get out of the situation - ask again, or set a default value. Or return the special value already to the one who caused it, reporting failure.
In many such cases, when errors occur frequently and the code calling the function must take them into account, it is perfectly acceptable to return a special value as an error indicator. But there are also disadvantages. First, what if a function can return any type of value anyway? It is difficult for her to find a special value that will differ from an acceptable result.
The second problem - working with special values can trash the code. If the promptNumber function is called 10 times, then you need to check 10 times if it returned null. If the response to null is to return null to a higher level, then where this code was called, you also need to embed a check for null, and so on.
Exceptions
When the function cannot work normally, we would like to stop the work and jump to where such an error can be processed. This is handled by exception handling.
The code that encountered the problem at the time of execution can raise (or throw) an exception (raise exception, throw exception), which is some kind of value. The return of an exception resembles a kind of "pumped" return from a function — it jumps not only from the function itself, but also from all the functions that called it, to the place from which the execution began. This is called unwinding the stack. Perhaps you remember the stack of functions from Chapter 3 ... The exception quickly flushes the stack down, throwing out all the contexts of the calls that it encounters.
If the exceptions immediately reached the very bottom of the stack, there would be little benefit from them. They would just provide an interesting way to blow up the program. Their strength lies in the fact that on their way in the stack you can put "obstacles" that will catch exceptions, rushing along the stack. And then with this you can do something useful, after which the program continues to run from the point where the exception was caught.
Example:
function promptDirection(question) { var result = prompt(question, ""); if (result.toLowerCase() == "left") return "L"; if (result.toLowerCase() == "right") return "R"; throw new Error(" : " + result); } function look() { if (promptDirection("?") == "L") return ""; else return " "; } try { console.log(" ", look()); } catch (error) { console.log("- : " + error); }
The throw keyword is used to throw an exception. The catch is a piece of code wrapped in a try block followed by catch. When the code in the try block throws an exception, the catch block is executed. The variable in brackets will be bound to the exception value. After the execution of the catch block is completed, or if the try block is executed without any problems, the execution proceeds to the code behind the try / catch instruction.
In this case, we used the Error constructor to create an exception. This is a standard constructor that creates an object with the message property. In modern JavaScript environments, instances of this constructor also collect information about the call stack that was accumulated at the time the exception was thrown - the so-called stack trace. This information is stored in the stack property, and can help with the analysis of the problem — it tells you in which function the problem occurred and what other functions led to this call.
Note that the look function completely ignores the possibility of problems in promptDirection. This advantage of exceptions - the code that handles errors is needed only at the place where the error occurs, and where it is processed. Intermediate functions simply do not pay attention to it.
Almost.
Clean up with exceptions
Imagine the following situation: the withContext function wants to make sure that when it is executed, the top-level variable context gets a special context value. After completion of execution, it restores its old value.
var context = null;
function withContext(newContext, body) { var oldContext = context; context = newContext; var result = body(); context = oldContext; return result; }
What if the body function throws an exception? In this case, the call withContext will be thrown out of the stack exception, and the context value will never be returned to its original value.
But the try instruction has another feature. It can be followed by a finally block, either instead of catch, or with catch. The finally block means “execute the code anyway after the try block has been executed”. If functions need something to clean up, then the cleaning code should be included in the finally block.
function withContext(newContext, body) { var oldContext = context; context = newContext; try { return body(); } finally { context = oldContext; } }
Notice that we no longer need to store the result of the body call in a separate variable to return it. Even if we return from the try block, the finally block will still be executed. Now we can safely do this:
try { withContext(5, function() { if (context < 10) throw new Error(" !"); }); } catch (e) { console.log(": " + e); }
Despite the fact that the function called from withContext is “broken”, withContext itself still clears the value of the context variable.
Selective catch exceptions
When an exception reaches the bottom of the stack and no one has caught it, the environment handles it. How exactly - depends on the specific environment. In browsers, a description of the error is displayed in the console (it is usually available in the "Tools" or "Development" menu).
If we are talking about errors or problems that the program cannot handle in principle, it is permissible to just skip such an error. An unhandled exception is a reasonable way to report a problem in a program, and the console in modern browsers will give you the necessary information about which function calls were on the stack at the time the problem occurred.
If the occurrence of a problem is predictable, the program should not fall with an unhandled exception - this is not very user-friendly.
Invalid use of a language — references to a non-existent variable, a property request on a variable equal to null, or a call to something that is not a function, also result in throwing exceptions. Such exceptions can be caught just like your own.
When entering a catch block, we only know that something inside the try block led to an exception. We do not know what exactly, and what exception occurred.
JavaScript (which is a flagrant omission) does not provide direct support for selective catching exceptions: either we catch everything or none. Because of this, people often assume that the exception that occurred is precisely the one for which the catch block was written.
But it can be different. The violation happened somewhere else, or an error crept into the program. Here is an example where we try to call promptDirection until we get a valid answer:
for (;;) { try { var dir = promtDirection("?");
The for (;;) construct is a way to arrange an infinite loop. We fall out of it only when we get a valid direction. But we incorrectly wrote the name promptDirection, which leads to the error “undefined variable”. And since the catch block ignores the value of the exception e, assuming that he is dealing with another problem, he believes that the exception thrown is the result of incorrect input data. This leads to an infinite loop and hides a useful error message about the wrong variable name.
As a rule, you should not catch exceptions so much, unless you have a goal to redirect them somewhere - for example, over the network, to inform the other system about the fall of our program. And even then carefully look, whether the important information will not be hidden.
So, we need to catch a certain exception. We can check in the catch block whether the exception that has occurred is the exception of interest to us, and otherwise re-throw it. But how do we recognize an exception?
Of course, we could compare the message property with the error message we are waiting for. But this is not a reliable way to write code — use information meant for a person (a message) to make a software decision. As soon as someone changes or translates this message, the code will stop working.
Let's better define a new type of error and use instanceof to recognize it.
function InputError(message) { this.message = message; this.stack = (new Error()).stack; } InputError.prototype = Object.create(Error.prototype); InputError.prototype.name = "InputError";
The prototype is inherited from Error.prototype, so the instanceof Error will also be executed for objects of type InputError. And the name property is assigned to it, as well as other standard types of errors (Error, SyntaxError, ReferenceError, etc.)
Assigning the stack property tries to pass a stack trace to this object, on those platforms that support it, by creating an Error object and using its stack.
Now promptDirection can create such an error.
function promptDirection(question) { var result = prompt(question, ""); if (result.toLowerCase() == "left") return "L"; if (result.toLowerCase() == "right") return "R"; throw new InputError("Invalid direction: " + result); } . for (;;) { try { var dir = promptDirection("?"); console.log(" ", dir); break; } catch (e) { if (e instanceof InputError) console.log(" . ."); else throw e; } }
The code catches only instances of InputError and skips other exceptions. If you make the same typo again, the message about the undefined variable will be displayed correctly.
Assertions
Assertions are a tool for simple error checking. Consider the assert helper function:
function AssertionFailed(message) { this.message = message; } AssertionFailed.prototype = Object.create(Error.prototype); function assert(test, message) { if (!test) throw new AssertionFailed(message); } function lastElement(array) { assert(array.length > 0, " lastElement"); return array[array.length - 1]; }
This is a compact way to tighten the value requirements, which throws an exception when the specified condition is not met.
For example, the lastElement function, which extracts the last element of an array, would return undefined for empty arrays if we did not use assertion. Extracting the last element of an empty array does not make sense, and this would clearly be a programmer's mistake.Assertions are a way to make sure that errors provoke the interruption of a program in the place where they are committed, and not just give out strange values that are transmitted through the system and cause problems in some other unrelated places.Total
Errors and invalid input data occur in life. Errors in the programs must be searched and corrected. It is easier to find them using automatic checking systems and adding statements to your programs.Problems caused by something that is not within your program need to be handled adequately. Sometimes, when a problem can be solved locally, it is permissible to return special values to track such cases. In other cases, it is preferable to use exceptions.Throwing an exception causes the stack to unwind until a try / catch block is encountered or until we reach the bottom of the stack. The exception value will be passed to the catch block, which will be able to verify that the exception is really what it is waiting for and handle it. To work with unpredictable events in the program flow, you can use finally blocks so that certain parts of the code are executed anyway.Exercises
Replay
Suppose you have a function primitiveMultiply, which in 50% of cases multiplies 2 numbers, and in other cases throws an exception like MultiplicatorUnitFailure. Write a function that wraps this one and just calls it until a successful result is obtained.Make sure that you handle only the exceptions you need. function MultiplicatorUnitFailure() {} function primitiveMultiply(a, b) { if (Math.random() < 0.5) return a * b; else throw new MultiplicatorUnitFailure(); } function reliableMultiply(a, b) {
Locked box
Consider this rather contrived object: var box = { locked: true, unlock: function() { this.locked = false; }, lock: function() { this.locked = true; }, _content: [], get content() { if (this.locked) throw new Error("!"); return this._content; } };
This is a box with a lock. Inside is an array, but it can only be reached when the box is not locked. You cannot directly access the _content property.Write a withBoxUnlocked function that takes a function as an argument that unlocks the box, performs the function, and then always locks the box again before exiting - no matter whether the function passed correctly or if it threw an exception. function withBoxUnlocked(body) {
As a bonus game, make sure that when you call withBoxUnlocked, when the box is not locked, the box remains unlocked.