Modularity and Abstraction in C++ Programs
Something has gone wrong while the program is running, and the program now has to respond.
A Program That Works in Testing
Imagine a program that works well during testing but fails in the field. A file it expected is missing, a network connection drops in the middle of a task, or a value arrives in a shape nobody anticipated. Something has gone wrong while the program is running, and the program now has to respond.
This article is about a question that follows from that situation. It is not only how to respond to a problem, but which part of the program should be responsible for responding, and how that responsibility is divided between the code that notices the problem and the code that decides what to do about it. The answer depends on how the program is organized into modules and abstractions, so the two ideas are taken together here.
The subject is large. Error handling reaches well beyond what a programming language's built-in features can address, into programming techniques and tools. The language does still provide some help, and the most important of that help comes from the type system. Both the separation between detecting and handling errors and the type system's role in preventing them are covered below.
Watching an Error Climb the Call Chain
Programs are built from modules, and modules call one another. Picture a chain of calls: the main part of the program calls application logic, the application logic calls a library function, and the library function calls an internal step that does a piece of the library's specific job. When the internal step finds that something has gone wrong, it is the first place that knows about the problem. The question is what happens next.
The diagram shows four frames stacked in the order they were called. The lowest frame is where the problem is detected, because that is where the relevant knowledge lives. The frames above it are where the decision about what the problem means for the whole application can be made. Control has to travel back up through the chain for the problem to reach the code that can respond.
What do you think happens?
In the call chain above, which frame is best placed to decide what the whole application should do about a failed lookup?
Reveal answer
The application logic, because it knows the application's goals
The source describes the code that detects a problem as being in the best position to notice it, but frequently not in the best position to decide what the overall application should do. The code that calls the library usually has that context.
Why Modularity Separates Detection from Handling
The central idea of this part of the material is that the point where a run-time error can be detected is often separated from the point where it can be handled. These two points are not automatically the same line of code, and they are often not even in the same part of the program. The source attributes this separation to modularity and abstraction.
Modularity is the practice of dividing a program's work into modules, where a module often hides its internal details behind an interface. An interface is the path through which other parts of the program interact with the module.
Consider a library written to perform a specific job. The code inside that library is in the best position to notice that something has gone wrong with its own work. It is frequently not in the best position to decide what the overall application should do about it. That decision depends on the application's goals, and those goals are known to the code that calls the library. Modularity is what makes the code clean, and it is also what creates the gap between noticing a problem and responding to it.
Detection happens inside the module, where the relevant knowledge lives. Handling happens outside it, where the application's goals are known. The interface between them is the path the problem must travel.
A small program often has one author and one place where everything happens, so the detection point and the handling point usually sit close together. The gap becomes significant as the program grows, its code is split into modules, and it relies heavily on libraries written by other people.
Following One Error from Detection to Handling
Now trace a single error step by step. The question for each step is the same: does this piece of code have the context needed to choose a response, or should it pass the problem on? The flowchart below follows a failed lookup from the internal step where it is detected to the application code where it can be handled.
A library lookup that fails in the field
An application uses a library to look up a configuration value. During testing the value was always present. In the field, the lookup fails. Trace where the failure is detected, where it passes through, and where it can be handled.
- 1Detect the failure
The internal lookup step inside the library is the first code to learn that the value is not there. It has the knowledge of how its own lookup works, so it is the right place to notice the problem.
- 2Pass the problem outward
The library function does not know whether a missing value should stop the application, fall back to a default, or be reported to the user. It cannot decide the response from its own context, so the problem moves up the call chain.
- 3Handle the problem
The application code is the first frame that knows what the lookup was for and what the program is trying to accomplish. It is the code in a position to choose the response.
The failure was noticed in the internal lookup step, passed through the library function without a decision, and handled in the application code. The detection point and the handling point were in different modules.
Mistakes Beginners Make About Where Errors Go
Common mistake Assuming the code that finds a problem should also decide the response
What it looks likeAn internal step detects a missing value and immediately chooses a default value for the whole application.
Why it failsThe internal step does not know the application's goals, so the default it chooses may be wrong for the caller.
Do this insteadLet the detecting code report the problem through the module's interface, and let the caller that has the context choose the response.
Common mistake Thinking the detection point and the handling point are always the same line
What it looks likeLooking for the handling code only at the spot where the failure first shows up.
Why it failsThe source states these two points are not automatically the same line and often not even in the same part of the program.
Do this insteadTrace the problem through the call chain to find the frame that has the context to respond.
Common mistake Believing modularity only makes code tidier and has no effect on error handling
Common mistake Expecting the program's built-in features to handle every error automatically
Why it failsThe source states that error handling is a large topic reaching into programming techniques and tools, beyond what the language's built-in features can address.
Do this insteadDesign the handling responsibilities deliberately as part of the program's structure.
How the Type System Prevents Mistakes
Detecting and handling errors is only one part of the story. The source identifies the type system as the major tool C++ offers for preventing errors in the first place. Higher-level types and algorithms limit the opportunities for mistakes and help the compiler catch the ones that remain.
The source describes two ways of building a program. In the first, all data is stored in the most basic containers the language provides, such as single characters, plain integers, and floating-point numbers, and all behavior is assembled from basic statements like conditionals and loops. In the second, the programmer first defines richer types that match the application, such as a string type, an associative map type, and a pattern-matching type, and then defines algorithms that operate on those richer types. The second approach is the one the source describes as the major tool for error prevention.
The reason the richer types help is that higher-level constructs make certain mistakes hard to write in the first place. A well-designed type limits the operations that make sense on it. A programmer working with the right abstraction is unlikely to attempt an operation that belongs to an entirely different domain. The source's example is that you are unlikely to apply a tree traversal to a dialog box. Because types and algorithms are shaped around the application, there are fewer chances to combine them incorrectly.
Higher-level types also give the compiler more to work with, and the source notes that such constructs increase the compiler's chances of catching errors. Prevention therefore happens at two levels. The programmer chooses abstractions that rule out many mistakes, and the compiler checks the remaining uses against the declared types.
The type system prevents errors before the program runs by making invalid uses hard to write. Detection and handling then cover the errors that prevention cannot rule out, such as a file that is missing at run time.
Why Shared Standards Matter as Programs Grow
As programs grow and rely on libraries, standards for handling errors become important. The source notes that this is especially true when libraries are used extensively, because the code is then spread across many modules that different people wrote.
Without shared standards, each module may invent its own way of signaling problems, and the people writing the calling code may not know what to expect. A consistent approach means that when an error is detected in one module, the rest of the program has a predictable way to learn about it and respond. The handling side can then be written against a known pattern rather than guessed at.
Designing the Strategy Early
The source recommends designing and stating a strategy for error handling early in a program's development. The reason is that the separation between detection and handling is built into the program's structure. Once modules, interfaces, and libraries are in place, the question of how problems travel across them is already partly answered, whether or not anyone chose the answer deliberately.
Decide early which modules detect which kinds of problems, how those problems are passed through interfaces, and which part of the application is responsible for each response. Write these decisions down so that every module author works from the same rules.
Practice Tracing an Error
A program has four layers: a user interface layer, an application layer, a data access module, and a low-level file reader. The file reader finds that a required file is missing. Trace where the error is detected, which modules it passes through without a decision, and which layer is in the best position to choose a response such as showing a message or using a default. Justify your answer using the idea of modularity and the context each module has.
Hints
- Start with the code that first learns the file is missing, and ask whether it knows what the user was trying to do.
- Each module hides its internal details behind an interface, so ask what each layer can see about the overall goal.
- The source says the module that detects a problem often lacks the context needed to choose a response.
In one sentence, explain why a module that detects a problem might still be the wrong place to decide the response, and name the kind of information the handling code needs that the detecting code may lack.
Hints
- Think about the application's goals rather than the module's internal work.
Key Takeaways
- A run-time error is often detected in a different place from where it can be handled, because modularity hides details inside modules and the detecting code often lacks the context to choose a response.
- Tracing an error means following it through the call chain, passing through intermediate code that has no decision to make, until it reaches the code that knows the application's goals.
- The type system is the major tool C++ offers for preventing errors, since higher-level types limit the operations that make sense and give the compiler more chances to catch mistakes.
- As programs grow and rely on libraries, shared standards for handling errors matter, and the source recommends designing the strategy early in development because the separation between detection and handling is built into the program's structure.