Concepts / Error Handling Strategy in Large Programs

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.

  • Programming

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?

  • Rejects the program, because the two values describe different things

  • Accepts the addition silently, because both are built-in numeric types

Reveal answer
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

cpp
Worked example

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.

  1. 1
    Step 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.

  2. 2
    Step 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.

  3. 3
    Step 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.

  4. 4
    Step 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.

Result

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.

Type systemApplication typesBuilt-in typesAlgorithmsFewer mistakeslets you buildlets you buildbuilt fromoperate onhelps prevent
How do the custom types, the algorithms that operate on them, and the built-in types relate to one another 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.

mainhandles the errorprocess_ordercalls the libraryparse_quantitydetects the problemcallscallserror reportederror passed up
How does an error detected deep inside one function travel back up the chain of calls to the place that actually handles it?

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 like

    Storing a customer count and a length in meters as two plain int or double values and adding them together.

    Why it fails

    Plain values state nothing about their purpose, so the compiler accepts operations that have no meaning for the program.

    Do this instead

    Define 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 like

    A 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 fails

    Detection and handling are often in different places, so the library cannot rely on the caller knowing what to do about every failure.

    Do this instead

    Decide, 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 like

    Writing many modules, each with its own ad hoc approach to failures, and only later trying to unify them.

    Why it fails

    Large programs have many detection and handling points, and without an agreed approach those points drift apart.

    Do this instead

    Design 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

MEDIUM

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

  1. 1
    Higher-level types limit mistakes by making wrong pairings unlikely to be written in the first place.
  2. 2
    Higher-level types give the compiler more declared information, so it can check whether each use matches the purpose of the value.
  3. 3
    Modularity 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.
  4. 4
    A 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.

Related concepts

More concepts starting with E