Skip to content

← Errors step 4 of 4

Medium Primitives

The handler that hid the failure

There are three honest answers to “this can go wrong”, and the skill is telling which one you are looking at.

The situation The answer
An outcome the caller should expect and act on Return it. std::optional, an error enum, std::expected in C++23.
A failure the caller cannot prevent and may want to recover from Throw it. The disk, the network, an allocation.
A broken assumption inside your own program Refuse to continue. assert, or terminate.

The third row surprises people, so it is worth being blunt about. If a function’s precondition is violated — a null pointer where the contract said non-null, an index the caller was told to bound, an invariant that cannot be true — then your program has a bug, and it is now operating on state nobody designed for. Continuing means computing a wrong answer and writing it somewhere. Stopping means a stack trace and a fix.

assert(index < size_);   // a claim about the program, removed by NDEBUG
if (!file) throw IoError{...};   // a claim about the world, always checked

assert is for things you believe cannot happen. Exceptions are for things you know can.

Which brings us to catch (...)

try {
    everything();
} catch (...) {
    return 0;
}

This is the shape that turns a bug into a wrong number. It catches the failure you anticipated, and also the null dereference, the failed allocation, the invariant violation and the exception a colleague added last month — and reports all of them as a plausible-looking zero, at a call site with no idea anything happened.

Two rules follow, and they are the whole lesson:

Catch the specific types you know how to handle. If you cannot say what you would do about it, you cannot handle it, and catching it only hides it from someone who could.

Scope the try to the smallest thing that can fail. A try around a whole loop cannot resume; a try around one iteration can.

catch (...) has exactly one legitimate use: logging at a boundary and then throw; to rethrow. It is never a recovery.

Your task

std::vector<long long> tally(const std::vector<std::string>& fields);

Each field should be an integer. Return two numbers: the sum of the ones that parse, and how many did not.

std::stoll throws std::invalid_argument when the text is not a number, and std::out_of_range when it will not fit. Those are the two you know how to handle: skip the field and count it.

The starter wraps the whole loop and answers every failure with a zero, so one bad field discards every good one before it.