Concepts / Exception Handling in C++

Exception Handling in C++

Develop an error-handling strategy early in a design, as part of deciding the program's structure.

  • Programming
Interactive lab

Try it: try / except / else / finally

How an exception jumps out of a try block to the first matching except clause, when else runs, and why finally always runs.

How it works

  1. Statements in try run until one raises an exception; the rest of try is skipped.
  2. Python checks the except clauses in order and runs the first one whose type matches.
  3. If nothing was raised, the else block runs instead.
  4. finally runs in every case.

Default run (9 steps): Call safe_divide('4'). … Return 25.0. Output was: Worked: 25.0 / Done.

Simplified: One fixed function; int() and / are modelled exactly (including how Python prints floats), but nothing is executed as Python.

Educational simulation

Loading the simulation…

Why Error Handling Is a Design Decision

Every non-trivial program will eventually meet a situation it did not expect. A value may be out of range, a resource may be missing, or a request may not be completable. How a program responds to these situations is a design decision, not an afterthought. This article looks at three principles from the C++ modularity guidance on error handling: design an error-handling strategy early, use purpose-designed user-defined types as exceptions, and do not try to catch every exception in every function.

Before reading the principles, it helps to test the instinct most beginners bring to this problem. Once you have seen why that instinct fails, the three principles will read as fixes to a specific mistake rather than as a list of rules to memorize.

Testing the Catch-Everything Instinct

A common first reaction to errors is to wrap every function in a protective handler, so that nothing can ever escape unnoticed. It feels safe and thorough. The guidance you are studying treats this as a misconception: the idea that the right way to handle errors is to catch every exception in every function.

The misconception is attractive because it looks like diligence. But it moves the handling decision away from the place where the program actually knows what to do about a problem. A function deep in the call chain usually knows that a value is wrong, but it rarely knows whether the right response is to ask the user again, skip one item, stop the whole batch, or log and continue. A handler written there has to guess.

Designing the Strategy Before the Code

The first principle is timing. An error-handling strategy is something you develop early in a design, not something you bolt onto a finished program. Deciding how errors will be represented, where they will be detected, and where they will be handled is part of shaping the program's structure.

When the strategy is left until the end, handling code tends to appear wherever a problem happens to surface. Each developer adds a catch where a crash showed up, and the result is a scattered pattern in which nobody can see the overall plan. Early design avoids this by answering three questions before implementation begins: how an error is represented, where it is detected, and where it is handled.

Best practice

During structural design, write down for each important failure case which layer detects it, which type represents it, and which layer is expected to respond. If you cannot name the responding layer, the design is not finished yet.

Purpose-Designed Exception Types

The second principle says to use purpose-designed user-defined types as exceptions, and explicitly not built-in types. The reason this matters is that a dedicated type names the error. A handler that catches an exception of a type called for this exact problem can tell what went wrong from the type alone.

A generic built-in value carries little meaning about the situation that produced it. An int that happens to be -3 does not say whether it was a quantity, a count, or an index. A purpose-designed type also gives the program a place to store the details that a particular handler needs, such as the rejected value itself.

Following One Error Up the Call Chain

The following example follows one error through a chain of function calls. A validation function detects a bad quantity and throws a purpose-designed exception. The function that calls it does not catch the error, because it has no meaningful response yet. Only the top-level caller decides what to do. The code below is an original illustration of that pattern, written for this article.

cpp
Output
Order rejected, quantity: -3
Notes on the code
  • InvalidQuantityError is a purpose-designed type: its name states the problem and its member stores the rejected value.
  • validate_quantity detects the problem and throws, but does not try to recover.
  • process_order has no catch block. It has no meaningful response, so it lets the exception travel onward.
  • main is the only place with a catch, because it is the layer that decides what to do about a rejected order.

What do you think happens?

When process_order(-3) throws, which function's catch block runs?

  • validate_quantity

  • process_order

  • main

Reveal answer
Answer:

main

validate_quantity and process_order both have no matching catch, so the exception travels up the call chain until main, which has a handler for InvalidQuantityError.

Tracing the Error Through the Call Stack

The diagram shows the calling chain the error travels through. Read it from the top: main called process_order, which called validate_quantity. The highlighted frame is where the throw happens. Control does not return normally from that frame; it moves up the chain until a handler is found.

mainhas catch handlerprocess_orderno catch, passes error onvalidate_quantitythrows InvalidQuantityErrorcallscalls
Which function frames does the error pass through, and which one holds the catch handler?

Notice what the example does not do. The validation function does not try to recover, and the middle function does not swallow the error. The purpose-designed type carries the problem and the rejected value up to the place that can act on it. This is the scoping idea in practice: the catch appears where a meaningful response exists.

Where Catching Belongs

The third principle is to not try to catch every exception in every function. The guidance does not tell you to never catch exceptions; it tells you not to catch them everywhere. Catching is appropriate at a point where the code knows enough to respond sensibly. That response might be recovering, reporting to the user, or cleaning up.

Where no such response is available, letting the exception travel to a function that has one is the cleaner choice. A useful test for any catch is to ask what it would do differently from the caller. If the answer is nothing, the catch belongs higher up.

throwInvalidQuantityErrorin validate_quantityRemaining statementsskippednormal return path nottakenprocess_order has nomatching catchexception continues upwardmain catches ithandler runsReport rejected ordermeaningful responsecontrol leavesunwinds to callermatching catch founddecides response
After a throw, which statements are skipped, and where does control go next?

Read the flowchart as the reverse of the normal return path. A normal function returns a value to its caller and continues there. After a throw, the statements in the throwing function that follow the throw are skipped, and control moves to the caller chain until a handler accepts the exception. In this example, the handler sits in main, so the rejected order is reported there.

Mistakes That Scatter Error Handling

  • Common mistake

    Wrapping every function in a catch block so nothing escapes

    What it looks like

    validate_quantity, process_order, and main each have their own try and catch, all reporting the same error.

    Why it fails

    The handling logic is spread across the whole program, so the overall strategy is hard to see. Each handler also has to guess what the caller wants.

    Do this instead

    Keep catch blocks only where a meaningful response exists, and let other exceptions propagate to that layer.

  • Common mistake

    Throwing a plain int or string for every error

    What it looks like

    throw -3; used to signal a bad quantity.

    Why it fails

    A built-in value does not name the problem. A catch clause that receives an int cannot tell a bad quantity from any other integer error.

    Do this instead

    Define a purpose-designed type such as InvalidQuantityError, and store the relevant detail, such as the rejected value, inside it.

  • Common mistake

    Catching an exception and silently discarding it

    Why it fails

    The error disappears, and the program continues as if nothing happened. The guidance treats this as removing the problem from view rather than handling it.

    Do this instead

    Only catch where you can recover, report, or clean up. Otherwise let the exception travel to the layer that can.

  • Common mistake

    Deciding the error strategy only after the code is written

    Why it fails

    Handling code then appears wherever a failure happened to surface, and the structure of the program no longer reflects where problems should be handled.

Built-in Values Versus Dedicated Types

The comparison below contrasts the two approaches to representing an error. The question to ask of each is what a catch clause can learn from the exception alone.

AspectBuilt-in value (for example int or string)Purpose-designed user-defined type
What the type name tells a handlerOnly the underlying kind of dataThe specific problem, such as InvalidQuantityError
Details it can carryA single raw value with no defined meaningNamed members, such as the rejected quantity, that a handler needs
How a catch clause selects itAny catch for the same built-in type, which may match unrelated errorsOnly the catch that names this exact type
Fit with the guidanceExplicitly not recommended as the exceptionThe recommended choice for each error

Two ways to represent an error, compared by what a handler can learn from the exception.

Checking Your Placement Decisions

MEDIUM

A program reads a configuration file. Three functions are involved: read_config opens the file and detects a missing file, parse_config checks each line and detects a malformed value, and main decides whether to fall back to default settings or stop. Using the principles in this article, decide which of the three functions should contain a catch block, and explain why the other two should let the exception propagate. Also name the purpose-designed exception type you would define for the missing-file case.

Hints
  • Ask which function knows what a sensible response is: retrying, using defaults, or stopping.
  • A function that would only rethrow the same error without adding anything has no meaningful response yet.
  • Name the type after the exact problem, the way InvalidQuantityError names the bad quantity.

Key Takeaways

  • Develop the error-handling strategy early in the design, deciding how errors are represented, where they are detected, and where they are handled.
  • Use purpose-designed user-defined types as exceptions rather than built-in types, so each exception names its specific problem and can carry the details a handler needs.
  • Do not catch every exception in every function. Catch only where the code has a meaningful response, such as recovering, reporting, or cleaning up, and let other exceptions propagate up the call chain.
  • Catching everywhere spreads handling logic across the program and makes the overall strategy hard to see.

Related concepts

More concepts starting with E