Skip to content

← Errors step 2 of 4

Medium Primitives

Catch exceptions without slicing their type

The supplied fetch function throws one of two error types. Both derive from AppError, so one handler can catch either. How that handler declares its parameter determines whether it can still observe the specific error type.

Catch by const reference

The starter uses:

catch (AppError e) {
    return e.code();
}

If fetch throws NotFound, this creates a separate AppError object from the base part of the exception. That new object's type is AppError, so code() returns 500. The original exception is still a NotFound; the handler's local copy has lost the derived behavior. This is object slicing.

A reference avoids creating that base-only copy:

catch (const AppError& e) {
    return e.code();
}

Now e refers to the base part of the original exception, whose dynamic type remains NotFound or Denied. Virtual dispatch calls that derived type's code() override. const makes this access read-only.

The issue is the separate base object, not a general rule that virtual functions cannot be called on local objects. A standalone NotFound object also calls its own override. Copying it into a standalone AppError changes which object the later call uses.

Throwing and rethrowing

Prefer throwing an exception object by value, such as throw NotFound{};. throw new NotFound{}; throws a pointer instead, which the AppError reference handler does not catch and whose allocated object needs separate ownership.

Within a handler, throw; rethrows the currently handled exception and retains its dynamic type. In contrast, throw e; creates a new exception from the expression's static type. If e is declared as const AppError&, that new exception is an AppError even though the reference still refers to a NotFound. Use a bare rethrow when the original exception should continue outward.

Handlers are considered in source order. Put a specific derived-type handler before a base-type handler if they need different treatment. A final catch (...) can handle any exception at an appropriate boundary, translate a failure or rethrow it. Its behavior should follow the surrounding API's error contract.

Choosing and measuring an error mechanism

Whether a missing key is an error depends on the API: a lookup may return an optional result, while an operation that requires the key may report failure. There is no universal rule forbidding exceptions in loops. Avoid using them for routine iteration when a normal condition expresses the operation clearly.

Exception cost depends on the implementation, compiler options and workload. Throwing can involve exception-object storage, finding a handler and unwinding through destructors. C++ does not promise a fixed cost ratio or that enabling exceptions leaves the normal path unchanged. Measure relevant code before making performance claims. This exercise checks behavior, not exception timing.

Your task

Correct handle's catch parameter. Leave its return expressions, fetch and the exception classes unchanged:

int handle(int request);
Request fetch behavior Required handle result
Less than 0 Throws Denied 403
0 through 100, inclusive Returns request * 10 That returned value
Greater than 100 Throws NotFound 404

Inputs fit int. Successful multiplication is limited to 0..100, so it cannot overflow int. For requests [5, -1, 200, 100], the harness returns [50, 403, 404, 1000]. It calls handle separately for each request; a handled failure does not skip later requests. An empty request list produces [].

The starter returns 500 for both application errors because it slices their type into AppError. Preserve their dynamic type when catching them. Do not change the exception codes or replace the supplied fetch behavior.