Error Handling Strategy in Large Programs
The program is wrong in a way that only a human who knows what the numbers mean can detect, and that is precisely the gap that higher-level types are meant to close.
When Plain Numbers Start to Mislead
Imagine building a program entirely out of the most basic pieces a language offers: single characters, plain integers, floating-point numbers, and simple if, while, and for statements. Every piece of data is just a number or a character, and every operation has to be assembled by hand from those raw parts. Nothing in that design tells you whether a particular number is a customer count, a measurement in meters, or a position on a screen. Any of those values can be mixed into an operation that makes no sense for it.
What do you think happens?
A customer count is stored in a plain int, and a length in meters is stored in a plain double. If the program adds them with the + operator, what does the compiler do?
Reveal answer
The compiler accepts the addition silently.
Both values are built-in numeric types, so the arithmetic is legal as far as the compiler is concerned. Nothing in the declared types says that one value is a count of people and the other is a distance, so the mismatch passes without any warning. This is exactly the kind of mistake that plain values allow.
The question this article answers is how a language's type system can push back against that kind of mistake, and what that means for the way you structure a program from the start. The short version is that the type system is the major tool C++ provides for preventing mistakes. It works by letting you build types and algorithms suited to your application instead of relying only on built-in types and basic statements.
Tracing a Mix-Up With Raw Values
Following the values through a mixed-up sum
Trace what the program knows about each value at every step, and find the point where the design fails to protect the programmer.
- 1Step 1: Declare the first value
customers is an int holding 12. The declared type says only that it is an integer. It does not say that it counts people.
- 2Step 2: Declare the second value
length_m is a double holding 3.5. The declared type says only that it is a floating-point number. The name suggests meters, but the type itself does not carry that meaning.
- 3Step 3: Combine the values
The + operator is defined for both types, so the compiler has no reason to object. The expression is accepted as valid.
- 4Step 4: Inspect the result
total now holds a number that mixes a count of people with a length. The program runs without complaint, and the mistake is invisible to the type system.
The raw types gave the compiler nothing to judge the meaning of the operation. The only protection left was the programmer's memory of what each variable was supposed to mean.
Notice that the failure does not come from a syntax error or a typo. Each line is legal. The program is wrong in a way that only a human who knows what the numbers mean can detect, and that is precisely the gap that higher-level types are meant to close.
How Higher-Level Types Prevent Mistakes
Higher-level constructs simplify programming, limit opportunities for mistakes, and increase the compiler's chances of catching errors. The two benefits are connected, but they work in different ways. One reduces how often a wrong pairing gets written at all. The other gives the compiler more information to check the pairings that do get written.
Custom Types, Algorithms, and Built-In Types
Most of C++'s constructs exist to support the design of elegant and efficient abstractions, such as user-defined types and algorithms that work with them. Built-in types such as integers and characters are not discarded. They become the raw material from which application types are built, and the algorithms operate on those application types. The diagram below shows how these pieces relate in a larger program.
Tracing an Error From Detection to Handling
Modularity and abstraction, especially through libraries, can separate the point where a run-time error is detected from the point where it is handled. Code that notices a problem may sit deep inside a library, while the decision about what to do about the problem belongs to code somewhere else in the program. The diagram traces that path through a small call chain.
Follow the path in the diagram. The problem is noticed in parse_quantity, which is buried inside a library. That function cannot decide on its own whether the whole program should stop, retry, or continue. The error has to travel back up to a caller that holds that knowledge, and only then is it handled. The detection point and the handling point are separated by at least one call boundary.
Why Detection and Handling Drift Apart
Once detection and handling are in different places, a programmer who writes a library cannot simply assume the code that calls it will know what to do about every failure. The way errors are reported across that boundary has to be decided deliberately. This is why the source treats error handling as a design concern rather than a last-minute addition.
The source observes that standards for handling errors become important as programs grow, especially when libraries are used extensively. Its recommendation is to design and articulate a strategy for error handling early in the development of a program. A small program may have only a few places where problems are detected and handled, and a single author can keep them consistent in their head. A larger program, built from many libraries and modules, has many detection points and many handling points. Without an agreed approach, those points can drift apart, and each part of the program may quietly assume a different answer to the question of who deals with a failure.
Planning early changes the structure of the code written afterward. When the team has already decided how errors cross module boundaries, each new library or module is written to fit that decision. When the decision is postponed to the end, the detection points already scattered through the code have to be retrofitted, and the handling logic is forced to adapt to whatever was written first.
Mistakes Beginners Make With Error Handling
Common mistake Using plain numbers for every quantity in a program
What it looks likeStoring a customer count and a length in meters as two plain int or double values and adding them together.
Why it failsPlain values state nothing about their purpose, so the compiler accepts operations that have no meaning for the program.
Do this insteadDefine application-specific types such as Customers and Meters, so that operations between unrelated quantities are not defined.
Common mistake Assuming the caller of a library will handle every failure
What it looks likeA library function detects an invalid value and simply reports it, with no agreed way to pass the report up to the code that decides what to do.
Why it failsDetection and handling are often in different places, so the library cannot rely on the caller knowing what to do about every failure.
Do this insteadDecide, in advance, how errors are reported across each library boundary, and make that reporting rule part of the design.
Common mistake Postponing the error handling strategy until the program is large
What it looks likeWriting many modules, each with its own ad hoc approach to failures, and only later trying to unify them.
Why it failsLarge programs have many detection and handling points, and without an agreed approach those points drift apart.
Do this insteadDesign and articulate the strategy early, before the program grows, so that every new module follows the same rules.
Common mistake Believing that a type mistake is always reported as a syntax error
Practice: Spotting the Wrong Pairing
A program stores a dialog box and a tree as plain collections of integers and characters. A developer then applies a tree traversal to the dialog box. Explain why the plain-value design allows this mistake to go unnoticed, and describe how defining distinct types for a dialog box and a tree would make the mistake less likely to be written at all.
Hints
- Ask what the compiler can tell about a plain integer compared with a distinct application type.
- Think about whether a traversal has any meaningful structure to walk through in a dialog box.
- Consider whether a distinct type would make the traversal operation something you are unlikely to write.
Key Takeaways
- 1Higher-level types limit mistakes by making wrong pairings unlikely to be written in the first place.
- 2Higher-level types give the compiler more declared information, so it can check whether each use matches the purpose of the value.
- 3Modularity and libraries can separate where a run-time error is detected from where it is handled, so the reporting rules across that boundary must be designed deliberately.
- 4A strategy for error handling should be designed and articulated early, because large programs with many detection and handling points can drift apart without one.
At a glance
- Higher-level types limit programming mistakes by making wrong pairings unlikely to be written at all.
- Declared application types give the compiler more information, so it can reject uses that do not match a value's purpose.
- Detection and handling of an error can sit in different places, especially when libraries are involved, so reporting across that boundary must be designed on purpose.
- A handling strategy should be planned early, before a large program accumulates many detection and handling points that drift apart.