Learning try, throw, and catch is only the starting point. The harder API-design problem is creating a failure another function can use safely. In the parser below, a malformed optional discount produces a typed error. The caller inspects stable fields, applies a deliberate fallback, and commits saved state only after validation.
C++ exception handling is a non-local control-flow contract
Code in a try block calls functions normally until a throw creates an exception object and stops that path. The runtime searches outward for the nearest compatible catch, called the handler. Moving the failure through callers is propagation. Destroying locals in the crossed stack frames is stack unwinding. If no handler matches, the program terminates.
That mechanism does not decide policy. A low-level parser can detect that oops is not an unsigned integer, but it should not silently replace it with zero. It reports a typed failure. The caller that understands whether discount is optional decides to recover, retry, translate, or rethrow. This separation matters across the wider Coding & DSA path: an error mechanism carries facts, while the owning layer chooses what those facts mean for the operation.
Design a typed ParseError that carries stable facts
Derive the domain exception from std::runtime_error, following the type relationship in Object Oriented Technology Explained. Keep exactly four immutable public facts: field, offset, token, and expected. Do not throw an integer, C string, or bare std::runtime_error, because none gives this caller the fields it needs.
struct ParseError final : std::runtime_error {
const std::string field;
const std::size_t offset;
const std::string token;
const std::string expected;
ParseError(std::string f, std::size_t o, std::string t, std::string x)
: std::runtime_error("cannot parse field '" + f + "' at byte " +
std::to_string(o) + ": expected " + x + ", got '" + t + "'"),
field(std::move(f)), offset(o), token(std::move(t)), expected(std::move(x)) {}
};
unsigned parseUnsigned(std::string field, std::string_view token, std::size_t offset) {
unsigned value{};
const char* end = token.data() + token.size();
auto [ptr, ec] = std::from_chars(token.data(), end, value);
if (ec == std::errc{} && ptr == end) return value;
throw ParseError(std::move(field), offset, std::string(token), "unsigned integer");
}For the worked failure, the payload is "discount", 16, "oops", and "unsigned integer". Its what() text is exactly cannot parse field 'discount' at byte 16: expected unsigned integer, got 'oops'. The message is for diagnostics. Callers inspect fields, never parse that sentence. Requiring ptr == end also rejects "12x" instead of accepting the partial value 12.
Worked parser: recover from one malformed optional field
Use the plain ASCII input qty=12;discount=oops;total=48. Offsets are zero-based bytes. They also look like character positions here, but the API deliberately promises bytes.
Field | Token | Value starts at byte |
|---|---|---|
|
| 4 |
|
| 16 |
|
| 27 |
The caller parses required qty first, handles only the optional-field failure it owns, and commits once at the end:
unsigned qty = parseUnsigned("qty", "12", 4); // returns 12
unsigned discount = 0;
try {
discount = parseUnsigned("discount", "oops", 16);
} catch (const ParseError& e) {
if (e.field != "discount") throw;
std::cerr << e.what() << "; using discount=0\n";
}
unsigned total = parseUnsigned("total", "48", 27); // returns 48
Order candidate{1042, qty, discount, total};
savedOrders.push_back(candidate);The diagnostic is cannot parse field 'discount' at byte 16: expected unsigned integer, got 'oops'; using discount=0. The committed record is Order{id=1042, qty=12, discount=0, total=48}. Zero is the caller's documented fallback for this optional field, not a number invented by parseUnsigned.
Now change the input to qty=12x;discount=5;total=48. The call parseUnsigned("qty", "12x", 4) throws because from_chars stops before the trailing x. Since qty is required, parseOrder rethrows the failure and no Order{1042,...} is appended.

Stack unwinding and RAII clean up the failed path
For qty=12x;discount=5;total=48, follow runBatch -> importFile("order-1042.txt") -> parseOrder -> parseUnsigned("qty", "12x", 4). importFile constructs std::ifstream input first and std::vector<char> scratch(4096) second. parseOrder has local parsing state, but no Order candidate has been constructed or committed.
The exception is ParseError{field="qty", offset=4, token="12x", expected="unsigned integer"}. As it reaches runBatch, parseOrder locals are destroyed first. Next scratch releases its 4096-character buffer, then input closes the file while importFile unwinds. runBatch catches the error, rejects record 1042, and can continue with record 1043.
Acquire resources with RAII streams, containers, and smart pointers, which clean up in reverse construction order. Do not duplicate manual close() calls across catch branches, and do not let a cleanup destructor throw while unwinding.

Exception safety: build a candidate, then commit once
Exception safety describes what remains true when an operation fails:
Guarantee | Meaning | In this parser |
|---|---|---|
Basic | Invariants hold, although visible state may change |
|
Strong | Success, or no visible change | A required-field failure leaves |
No-throw | The operation promises not to emit an exception | Suitable only for operations that genuinely cannot fail and safe cleanup paths |
Before the call, let savedOrders = [Order{id=1041, qty=3, discount=5, total=60}]. Parse into local values and build candidate before savedOrders.push_back(candidate). With malformed required qty=12x, parsing fails before that commit point, so the vector remains exactly [Order{id=1041, qty=3, discount=5, total=60}].
With discount=oops, the caller chooses 0, finishes the candidate, and commits it once. The vector then contains records 1041 and 1042 exactly once each. Catching an exception did not create the strong guarantee. Delaying shared-state mutation until validation and recovery were complete created it.
Exceptions, returned errors, and assertions solve different problems
Failure handling is an API choice. The Programming Languages Survey shows how languages express such contracts differently.
Mechanism | Best fit | Applied to this parser |
|---|---|---|
Exception | A function cannot produce its promised value, and failure may cross several call layers | Throw |
Returned status or typed result | Expected branching whose outcome every caller should inspect | Return an error for optional |
Assertion | A programmer invariant, not user-input validation or recovery |
|
assert(token == "12") is wrong for either input case because external data can be malformed. Exceptions are not automatically superior to returned errors, and neither should hide useful facts. Whatever mechanism the API selects, provide machine-readable fields and make the mutation boundary obvious.
C++ exception traps and question forms that reveal them
Five corrections prevent common bugs:
Catch
const ParseError&to avoid copying the exception object. Catching a non-final exception hierarchy by value can also slice it.Put
catch (const ParseError&)beforecatch (const std::exception&). With that order, the first handler receives the worked exception. If the broader handler comes first, it handles the object and the specific branch is unreachable for that throw.Use bare
throw;to preserve the current exception.Do not use exceptions as a loop's normal success path.
Never let a destructor emit a second exception during unwinding.
A precise trace question might ask for cleanup after qty="12x": parseUnsigned and parseOrder locals, then scratch, then input. A state-guarantee question should leave the vector exactly [Order{id=1041, qty=3, discount=5, total=60}] after the required-field failure.
The short version and a deliberate next step
Keep four rules together: throw a typed object, catch at the layer that owns recovery, rely on RAII while frames unwind, and mutate shared state only after validation. That produces Order{id=1042, qty=12, discount=0, total=48} for the recoverable input and rejects required qty="12x". Change discount=oops first to discount=7, then to discount=7x, and predict the return or exception before running it. For structured C++ and placement-oriented coding, continue with Coding For Placements.




