Skip to content
VLSI Mentor

C++ Part I · Module 22 · Error Handling and Diagnostics

throw, try and catch

Chapter 22.1 established when a failure should be thrown; this chapter is how. The mechanism is three keywords and is genuinely simple, which is why most of the chapter is about the four details that are not. The first is the standard hierarchy: there is an existing tree rooted at std::exception, with a deliberate split between the failures a caller caused and the failures the environment caused, and deriving from the right branch tells a reader something before they have read your documentation. The second is that handlers are tried in order rather than best-match, so a base-class handler written above a derived one silently swallows everything. The third is the one the frozen purpose singles out, and it is the reason this chapter is worth measuring rather than describing: catching by value rather than by const reference copies the exception into a base-class object, which destroys exactly the derived information you threw it to convey. The chapter measures a TimeoutError carrying a timeout duration arriving at a handler as a plain DeviceError with that duration gone, and then measures which compiler flags warn about it — the answer being none of them, because the flag that exists for this belongs to a different compiler. The fourth is that rethrowing has two spellings and one of them repeats the same mistake.

Intermediate18 min readC++Part IFundamentalsError Handling

Part I · Module 22 · Chapter 22.2 · throw, try and catch

1. The Mechanism

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try {
    configure(device);                      // may throw
} catch (const std::runtime_error& e) {
    std::printf("failed: %s\n", e.what());
}

throw expr; constructs an exception object from expr and begins the search for a handler. try { … } marks a region whose handlers follow. catch (T) { … } handles an exception whose type matches T.

Three properties of throw are worth stating up front.

The thrown object is copied out of the throwing frame, into storage that survives unwinding. That is why throw localObject; is safe even though localObject is destroyed immediately afterwards — Chapter 22.3 measures the destruction.

Control does not return. Nothing after the throw in that function runs, and nothing after the call in any frame between there and the handler runs. Chapter 22.3 §1 measured zero statements after a propagating call being reached across three frames.

If no handler matches anywhere, std::terminate is called. An unhandled exception is not a silent failure; it is a loud one, which Chapter 22.1 §2 gave as a reason to prefer throwing for conditions nobody can handle.

2. The Standard Hierarchy

There is already a tree, and using it means every caller can catch your exceptions without knowing your types.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::exception                          <- the root; what() is here
  logic_error        <- a precondition the CALLER broke
    invalid_argument, domain_error, length_error, out_of_range
  runtime_error      <- something the environment did
    range_error, overflow_error, underflow_error, system_error
  bad_alloc, bad_cast, bad_typeid
  bad_optional_access, bad_variant_access, bad_function_call

The logic_error / runtime_error split is the useful part, and it mirrors Chapter 22.1 §1's categories. A logic_error means the caller passed something impossible — which by that chapter's test is usually a programming error and might be better served by an assert. A runtime_error means the world did not cooperate, which is the genuinely exceptional case.

So in practice: derive from runtime_error for operational failures, which is most of what you will throw.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class DeviceError : public std::runtime_error {
public:
    DeviceError(std::string msg, int code)
        : std::runtime_error(std::move(msg)), code_(code) {}
    int code() const noexcept { return code_; }
    virtual const char* kind() const { return "DeviceError"; }
private:
    int code_;
};
class TimeoutError : public DeviceError {
public:
    TimeoutError(int ms) : DeviceError("device timed out", 7), ms_(ms) {}
    int ms() const noexcept { return ms_; }
    const char* kind() const override { return "TimeoutError"; }
private:
    int ms_;
};

Two conventions in that code. The message goes to the base so what() works for a caller who only knows std::exception. And the data — the code, the duration — lives in your type, because what() returns a const char* and cannot carry structure.

One rule about what(): it is implicitly noexcept in practice and must not throw, so build the message string in the constructor rather than formatting it inside what(). Chapter 22.6 §5 is why that matters.

3. Handlers Are Tried In Order

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try { /* throws one of three things */ }
catch (const TimeoutError& e)    { /* most derived */ }
catch (const std::logic_error& e){ }
catch (const std::exception& e)  { /* most general */ }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TimeoutError&   : device timed out (250 ms, code 7)
logic_error&    : width must be 1..32
exception&      : index 12 of 8

The handlers are examined top to bottom, and the first one whose type matches wins. This is not overload resolution — there is no best-match rule and no ambiguity error.

The consequence is that ordering is semantic. Put catch (const std::exception&) first and it catches everything, and the handlers below it are dead code that the compiler is happy to accept. So: most derived first, most general last.

Note what the third line measured: an out_of_range caught as std::exception& kept its dynamic type — a dynamic_cast<const std::out_of_range*> on it succeeded. Catching by reference at a general level does not destroy information; §4 is about what does.

4. Catch By const Reference

This is the frozen purpose's specific instruction and the chapter's central measurement.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try { throw TimeoutError(250); }
catch (const DeviceError& e) { /* by reference */ }

try { throw TimeoutError(250); }
catch (DeviceError e)        { /* BY VALUE */ }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
by const&: kind() = TimeoutError  what() = device timed out
           dynamic_cast<TimeoutError*> = non-null (type intact)
by value : kind() = DeviceError   what() = device timed out
           dynamic_cast<TimeoutError*> = NULL -- the derived part is GONE

The by-value handler copy-constructed a DeviceError out of a TimeoutError. The copy is a DeviceError and nothing more: kind() reports the base, the dynamic_cast fails, and ms() — the 250 milliseconds that was the only genuinely diagnostic piece of information — does not exist in the object the handler is holding.

This is object slicing, the same mechanism Chapter 17.6 measured for passing a derived object by value. It is worse here, because an exception exists specifically to carry information about a failure, and the handler is the only place that information will ever be read.

And the detection result:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-Wall -Wextra -Wpedantic     total warnings=0   mentioning catch/slicing=0
-Weverything                 total warnings=3   mentioning catch/slicing=0
-Wcatch-value exists in clang? NO (unknown option -- it is a GCC flag)

No clang flag warns. -Weverything produced three warnings and none of them was about this — they were -Wweak-vtables and similar. GCC has -Wcatch-value, which does warn; clang rejects the option as unknown. So on this toolchain the only protection is the habit.

The rule, with no exceptions worth making: always catch (const T&). By reference so nothing is copied and nothing is sliced; const because a handler should not be modifying the exception, and because it lets you catch a const object. catch (T&) without const works and is a weaker promise. catch (T) is a bug.

5. Rethrowing: Two Spellings, One Correct

A handler that logs and passes the failure upward needs to rethrow, and the two forms are not equivalent.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try {
    try { throw TimeoutError(99); }
    catch (const std::exception&) {
        log("saw a failure");
        throw;                            // rethrow THE SAME object
    }
} catch (const TimeoutError& e) {
    /* e.ms() == 99 */
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
inner caught it; rethrowing with a bare `throw;`
outer caught TimeoutError& again: 99 ms -- type preserved

A bare throw; inside a handler rethrows the existing exception object, untouched. The outer handler saw a TimeoutError with its 99 milliseconds intact.

Now the other spelling:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
catch (const std::exception& e) { throw e; }      // throws a COPY of the base
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`throw e;` re-threw a SLICED copy: caught only as exception& (std::exception)

throw e; constructs a new exception object from e, and since e is declared const std::exception&, the new object is a std::exception — sliced all the way to the root. The outer catch (const TimeoutError&) did not match, and what() came back as the generic "std::exception".

So: throw; to propagate, throw something; only to replace. The second is legitimate when you deliberately want to wrap a low-level failure in a higher-level one, and then you construct a new object on purpose.

std::current_exception() and std::rethrow_exception() exist for moving an exception across a thread boundary or storing it, and std::nested_exception for chaining — all of which are beyond this chapter but worth knowing exist.

6. catch (...) Catches Everything and Tells You Nothing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try { throw 42; }
catch (const std::exception&) { /* not reached */ }
catch (...)                   { /* here */ }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exception& did NOT catch an int
catch(...) caught an int -- no type, no message

Anything copyable can be thrown, including an int. catch (...) matches all of it and gives you no name, no type and no what().

Two legitimate uses, both from Chapter 22.1 §8's constrained situations. At a C ABI boundary, where no exception may escape and everything must become a code. And in a cleanup-and-rethrow block:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
catch (...) { closeDevice(d); throw; }

— which Chapter 22.4 §3 measures, and then shows why you should not need it.

Everything else is a smell: a catch (...) that swallows is discarding information you cannot get back. And the complementary rule is always throw something derived from std::exception, so that a caller's general handler works and what() says something.

7. Where the Exception Object Lives

Worth a short section because it explains several of the rules above.

The thrown object is copied into storage managed by the runtime — not the throwing frame's stack, which is about to be unwound, and not normally the ordinary heap. For std::bad_alloc to be throwable when allocation has failed, there is an emergency buffer; __cxa_allocate_exception is the entry point. Chapter 22.6 §3's symbol dump shows the adjacent machinery — __gxx_personality_v0 and _Unwind_Resume — being referenced by any function a throw can pass through.

Three consequences.

Throw by value, catch by reference. throw TimeoutError(250); constructs a temporary that is copied into exception storage. There is nothing to manage.

Never throw a pointer. throw new TimeoutError(250); compiles and creates an ownership question with no good answer — the handler must know to delete it, and a catch (...) cannot.

The copy is why slicing happens. §4 and §5 are both the same mechanism: a copy into a base-class object loses the derived part.

8. What to Throw

A short checklist for designing an exception type.

Derive from std::runtime_error unless you have a reason not to. It gives you what() and a constructor taking a message.

Put the message in the base, the data in your type. code(), ms(), an address, a register name — whatever the handler needs in order to decide.

Build the message in the constructor. what() must not throw or allocate.

Make it cheap to copy. It will be copied at least once. A std::string member is fine; a 4 KB buffer is not.

Give it a noexcept copy constructor if you can. A throwing copy during a throw is a path to terminate.

One type per kind of failure, not per call site. Three types with a code field beat thirty types.

Document which functions throw which. The signature does not say — Chapter 22.6 is the only part of the type system that speaks about throwing at all, and it only says not.

9. The Rules Table

you writewhat happensverdict
catch (const T& e)binds to the exception, nothing copiedcorrect
catch (T& e)same, but you may modify itworks; weaker
catch (T e)copy-constructs a T — slices a derived exception (measured)bug, and nothing warns (measured)
catch (...)matches anything, gives no typeonly at a C boundary or to clean up and rethrow
throw; in a handlerrethrows the same object (measured: type preserved)correct
throw e; in a handlerthrows a copy of e's declared type (measured: sliced to std::exception)bug, unless replacing deliberately
throw T(args);constructs and throws a Tcorrect
throw new T(args);throws a T*; ownership unanswerablenever
throw 42;legal; only catch (...) or catch (int) sees it (measured)never
base handler listed firstcatches everything; later handlers deadbug; most derived first

10. What to Actually Do

  • catch (const T&), always. Measured: by value slices, and no clang flag warns.
  • Order handlers most-derived first. They are tried in order, not best-match.
  • Derive from std::runtime_error for operational failures, logic_error for broken preconditions — and reconsider whether the latter wants an assert.
  • Put the message in the base and the data in your type. what() cannot carry structure.
  • Build the message in the constructor, never inside what().
  • Use a bare throw; to propagate. throw e; slices.
  • Throw by value; never throw a pointer.
  • Always throw something derived from std::exception, so a general handler and what() work.
  • Limit catch (...) to a C boundary or a cleanup-and-rethrow — and prefer Chapter 22.4's RAII to the latter.
  • One exception type per kind of failure, with a code field, not one per call site.
  • If you also build with GCC, turn on -Wcatch-value. It is the only tool that catches §4.

11. Mistakes Worth Making Once

catch (DeviceError e). Measured: the derived part is gone, what() still works, nothing warns.

throw e; instead of throw;. Measured: sliced to std::exception, and the outer specific handler stopped matching.

catch (const std::exception&) as the first handler. Everything below it is unreachable and the compiler accepts it.

Formatting the message inside what(). It may allocate, it may throw, and it is called while an exception is already in flight.

throw new MyError(...). Ownership is unanswerable, and catch (...) cannot free it.

Throwing a bare int or const char*. No what(), no hierarchy, invisible to a general handler.

A catch (...) that swallows. The information is unrecoverable from that point on.

One exception class per call site. Thirty types that could have been three plus a code.

12. Interview Relevance

The frozen curriculum marks this chapter High, and the by-value question is the discriminating one.

"Why catch by const reference?" To avoid copying and, crucially, to avoid slicing a derived exception. The strong answer gives the measurement — a TimeoutError arriving as a DeviceError with the derived data gone — and adds that no clang flag warns about it.

"How are handlers selected?" In order, first match wins — not best match. So a base handler above a derived one makes the derived one dead code.

"What is the difference between throw; and throw e;?" The first rethrows the same object; the second throws a copy of e's declared type and slices. Measured.

"Which standard class would you derive from?" runtime_error for operational failures, logic_error for violated preconditions — and logic_error conditions are often really asserts.

"Can what() throw?" It must not. So build the message in the constructor.

"When is catch (...) appropriate?" At a C ABI boundary, and for cleanup-and-rethrow — and RAII removes the second.

"Where does the exception object live?" In runtime-managed storage, copied out of the throwing frame — which is why slicing is possible at all, and why throwing a pointer is wrong.

13. What You Should Be Able to Say Now

  • The three keywords, and that the thrown object is copied out of the frame being unwound.
  • The standard hierarchy and the logic_error / runtime_error split.
  • That handlers are tried in order, so most-derived comes first.
  • That catch (T) by value slices — measured, kind() reporting the base and the dynamic_cast returning NULL.
  • That no clang flag warns about it: -Weverything gave 3 warnings, none relevant, and -Wcatch-value is a GCC option.
  • That a bare throw; preserves the object and throw e; throws a sliced copy — measured as what() == "std::exception".
  • That catch (...) belongs at a C boundary or in a cleanup-and-rethrow, and nowhere else.
  • That what() must not throw, so the message is built in the constructor.
  • That you should never throw a pointer, and never throw something outside the hierarchy.

14. Exercises

Exercise 1 — the slice. Reproduce §4 with a three-level hierarchy and catch at each level, by value and by reference. Tabulate which data survives.

Exercise 2 — the missing warning. Try every warning flag your clang offers on the by-value catch. Then, if you have GCC, build the same file with -Wcatch-value and compare.

Exercise 3 — handler order. Write four handlers in the wrong order, confirm the program compiles, and determine which are unreachable. Then check whether any flag warns.

Exercise 4 — rethrow. Build both rethrow forms and verify the outer handler's behaviour with dynamic_cast in each case.

Exercise 5 — your own type. Design an exception for a bus error carrying address, response code and initiator. Put the message in the base, assert the type is noexcept-copyable, and use it from three call sites.

Exercise 6 — a throwing what(). Write a what() that allocates, throw it while another exception is unwinding, and record what happens.

Exercise 7 — the C boundary. Wrap a throwing C++ function for a C caller, converting every exception to an enum. Enumerate the categories you must handle and what you do with an unknown one.

15. Summary

  • throw copies the object out of the frame, try marks a region, catch handles a matching type; no handler anywhere means std::terminate.
  • The hierarchy is rooted at std::exception, with logic_error for a caller's broken precondition and runtime_error for an environmental failure — derive from the second for most things.
  • Handlers are tried in order, first match wins. Most derived first; a general handler above a specific one makes the specific one dead.
  • Measured: catch (DeviceError e) by value sliced a TimeoutError — kind() reported DeviceError and dynamic_cast<TimeoutError*> returned NULL, so the derived data was gone while what() still printed fine.
  • Measured: no clang flag warns. -Wall -Wextra -Wpedantic gave 0; -Weverything gave 3, none relevant; -Wcatch-value is a GCC flag clang rejects as unknown.
  • Measured: a bare throw; preserved the type (the outer handler saw TimeoutError, 99 ms), while throw e; threw a copy sliced to std::exception.
  • catch (...) caught a bare throw 42; that catch (const std::exception&) did not — so always throw within the hierarchy.
  • what() must not throw; build the message in the constructor.
  • Throw by value, catch by const reference, never throw a pointer.

The mechanism is now stated, including the fact that a throw runs destructors on the way out. That sentence hides the most consequential machinery in the language, and Chapter 22.3 is about it: the exception walks out through every frame between the throw and the handler, destroying each automatic object in exactly reverse construction order, through functions that have no try block and know nothing about the failure. The chapter measures five objects across three frames being destroyed in precisely the right order, and nothing after a propagating call being reached — which is the property that makes a destructor the only cleanup you can rely on.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.