Exception Handling in C++
Develop an error-handling strategy early in a design, as part of deciding the program's structure.
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
- Statements in try run until one raises an exception; the rest of try is skipped.
- Python checks the except clauses in order and runs the first one whose type matches.
- If nothing was raised, the else block runs instead.
- 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.
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.
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.
Order rejected, quantity: -3InvalidQuantityErroris a purpose-designed type: its name states the problem and its member stores the rejected value.validate_quantitydetects the problem and throws, but does not try to recover.process_orderhas 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?
Reveal 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.
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.
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 likevalidate_quantity, process_order, and main each have their own try and catch, all reporting the same error.
Why it failsThe 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 insteadKeep 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 likethrow -3; used to signal a bad quantity.
Why it failsA 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 insteadDefine 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 failsThe 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 insteadOnly 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 failsHandling 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.
| Aspect | Built-in value (for example int or string) | Purpose-designed user-defined type |
|---|---|---|
| What the type name tells a handler | Only the underlying kind of data | The specific problem, such as InvalidQuantityError |
| Details it can carry | A single raw value with no defined meaning | Named members, such as the rejected quantity, that a handler needs |
| How a catch clause selects it | Any catch for the same built-in type, which may match unrelated errors | Only the catch that names this exact type |
| Fit with the guidance | Explicitly not recommended as the exception | The 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
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
InvalidQuantityErrornames 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.