1. The job of a programmer
Very often novice programmers think of the work of a programmer completely differently than how experienced programmers think of it.
Beginners often saying something like "The program works, what else do you need?" An experienced programmer knows that "works correctly" is only one of the requirements for a program, and it's not even the most important thing!
The most important thing is that the program code is understandable to other programmers. This is more important than a correctly working program. Much more.
If you have a program that does not work correctly, you can fix it. But if you have a program whose code is incomprehensible, you can't do anything with it.
Just take the any compiled program, such as notepad, and change its background color to red. You have a working program, but you do not have understandable source code: it is impossible to make changes to a program like that.
A textbook example is when Microsoft developers removed the Pinball game from Windows because they could not port it to 64-bit architecture. And they even had its source code. They simply couldn't understand how the code worked.
Accounting for every use case
The second most important requirement for a program is to account for every scenario. Often times, things are a little more complicated than they seem.
How a novice programmer sees sending SMS messages:
How a professional programmer sees it:
The "works correctly" scenario is usually just one many possible. And that's why many newbies complain about CodeGym's task validator: only one scenario out of 10 works, and the newbie programmer thinks that's enough.
2. Abnormal situations
Abnormal situations may arise in the execution of any program.
For example, you decide to save a file but there is no disk space. Or the program is trying to write data to memory, but available memory is low. Or you download a picture from the Internet, but the connection is lost during the download process.
For each abnormal situation, the programmer (the author of the program) must a) anticipate it, b) decide how exactly the program should handle it, and c) write a solution that is as close as possible to the desired one.
That's why programs had very simple behavior for quite a long time: if an error occurred in the program, the program terminated. And that was a pretty good approach.
Let's say you want to save a document to disk, during the save process you discover that there is not enough disk space. Which behavior would you like the most:
- The program terminates
- The program continues to run, but does not save the file.
A novice programmer may think that the second option is better, because the program is still running. But in reality that is not so.
Imagine that you typed out a document in Word for 3 hours, but two minutes into your writing process it became clear that the program would not be able to save the document to disk. Is it better to lose two minutes of work or three hours?
If the program can't do what it needs to, it is better to let it close than continue to pretend that everything is okay. The best thing that a program can do when it encounters a failure that it cannot fix on its own is to immediately report the problem to the user.
3. Background about exceptions
Programs aren't the only ones that face abnormal situations. They also occur inside programs — in methods. For example:
- A method wants to write a file to disk, but there is no space.
- A method wants to call a function on a variable, but the variable is equal to null.
- Division by 0 happens in a method.
In this case, the calling method could possibly correct the situation (execute an alternative scenario) if it knows what kind of problem occurred in the called method.
If we are trying to save a file to disk and such a file already exists, we can simply ask the user to confirm that we should overwrite the file. If there is no available disk space, we can display a message to the user and ask the user to select a different disk. But if the program runs out of memory, it will crash.
Once upon a time, programmers pondered this question and came up with the following solution: all methods/functions must return an error code that indicates the result of their execution. If a function worked perfectly, it returned 0. If not, it returned an error code (not zero).
With this approach to errors, after almost every function call, programmers had to add a check to see if the function finished with an error. Code ballooned in size and came to look like this:
|Code without error handling||Code with error handling|
What's more, quite often a function that discovered that an error occurred did not know what to do with it: the caller had to return the error, and the caller of the caller returned it to its caller, and so on.
In a large program, a chain of dozens of function calls is the norm: sometimes you can even find a call depth of hundreds of functions. And now you have to pass the error code from the very bottom to the very top. And if somewhere along the way some function does not handle the exit code, then the error will be lost.
Another disadvantage of this approach is that if functions returned an error code, they could no longer return the results of their own work. The result of calculations had to be passed via reference parameters. This made the code even more cumbersome and further increased the number of errors.
GO TO FULL VERSION