Skip to content

← Errors step 2 of 4

Medium Primitives

The catch that threw half of it away

Exceptions are for the failures a caller cannot reasonably be asked to check on every call: the disk filled, the socket died, the invariant a constructor could not establish. They are not for “the key was not in the map”, which is an ordinary Tuesday and belongs in an optional.

What they cost

Modern implementations use table-driven unwinding. Entering a try block costs nothing at runtime — no register saved, no branch — and the normal path through a function that might throw is as fast as one that cannot. The cost lives entirely in the throw: allocating the exception, walking the stack, consulting tables, running destructors on the way. That is somewhere between hundreds and thousands of times the cost of returning a value.

Which gives the rule directly:

Free when it does not happen, ruinous when it does. So it must not happen often. An exception in a loop is a design error, not a slow path.

Throw by value, catch by const reference

throw NotFound{};                  // yes
throw new NotFound{};              // no: who deletes it?

catch (const AppError& e)          // yes
catch (AppError e)                 // no
catch (...)                        // only to log and rethrow

The middle one is what this problem is about. Catching by value copies the thrown object into a variable of the base type, and a copy of a base cannot hold a derived object. Everything the derived class added is gone, and the virtual functions dispatch to the base’s versions. The object was correct when it was thrown; it was truncated on arrival.

This is object slicing, and it is not specific to exceptions — it happens wherever a derived object is assigned to a base by value. Catch handlers are simply where it does the most damage, because the whole point of the hierarchy was to let the handler ask what went wrong.

throw NotFound{}          -> code() == 404
catch (AppError e)        -> e is an AppError. code() == 500.
catch (const AppError& e) -> e refers to the NotFound. code() == 404.

The reference binds to the actual object and changes nothing about it. It is also cheaper, which is the least interesting reason to prefer it.

Ordering and rethrow

Handlers are tried in source order, not by best match, so the most derived types go first — a catch (const AppError&) above a catch (const NotFound&) makes the second one dead code. Inside a handler, a bare throw; rethrows the original exception, preserving its dynamic type; throw e; throws a copy of the sliced e, which is the same bug again.

Your task

int handle(int request);

fetch is given. It returns a value, or throws NotFound (code 404) or Denied (code 403), both deriving from AppError (code 500).

Return the value on success, and the exception’s code on failure. Do not change fetch or the exception classes.

The starter catches and returns a code. Every failure comes back as 500.