95 / 163 · C++17 · 12 min
Error boundaries: reporting failure is not the same as handling failure
The parsing layer reports errors it can prove; the business layer decides how to respond. Use an amount-parsing interface to distinguish discovery, propagation, and translation, preserve the exception’s dynamic type, and ensure a failure result does not carry a fabricated amount.
In this lesson
C++ Coding Standards: 101 Rules, Guidelines, and Best Practices
Obtained the complete first-edition PDF and sequentially read the full text of all 101 items 0–100 (Summary, Discussion, Examples, Exceptions, and References), and inspected page by page all 80 substantive pages that contain embedded code images, filling in code blanks left by text extraction. Also cross-read official Items 1, 25, 73, 74, and 83. Summaries and the index are not counted as main text. auto_ptr, dynamic exception specifications, old-style adapters, and similar material are treated only as historical context; original examples use C++20 and do not copy old code.
Edition, actual reading range, and original sources →First decide who has enough information
When reading Item 74, the question worth asking is not “where can I write a catch,” but “what decision can this layer make.” A low-level parser can only judge whether the text is a non-negative integer; it cannot decide for the caller whether bad input is equivalent to zero. Treating failure as zero turns corrupted data into a real business record: the program appears to keep running, but the contract has already been broken.
The example’s parse_cents throws ParseError as soon as it finds a format or range error. It does not log, retry, or read external state; the caller can independently decide how to present the failure. Returning int means success; an exception means no amount was produced. The two paths must not be mixed.
Preserve facts when propagating; add semantics when translating
If an intermediate layer has no recovery action, it usually does not need a catch. Here we deliberately keep one forwarding layer that catches by const reference and then does a bare throw, so that an assertion can demonstrate the dynamic type is still ParseError; a real project should delete a forwarding layer that does nothing. Writing throw e creates a new exception determined by the expression’s static type and may lose derived-class information.
The business boundary submit translates a known parse failure into Status::bad_amount and leaves the optional amount empty. It only catches ParseError, which it can interpret, rather than swallowing unrelated failures such as out-of-memory with catch(...). Error translation is not flattening differences; it is putting the semantics the caller actually needs into the interface.
Modern interfaces still need a proof of the boundary
Item 73’s principle of throwing by value and catching by reference still applies in modern C++; C++17’s from_chars and optional are tools added for this lesson, not facilities already in the 2005 book. from_chars does not guarantee that the entire input is consumed, so both the error code and the end pointer must be checked; otherwise 12x might be mistaken for 12.
The example checks valid input, trailing garbage, and integer overflow at the same time. assert is used to prove this example’s conventions, not as a substitute for production input validation: even if assertions are disabled, the branches inside parse_cents still reject errors. If you switch to C++23 expected, you should still keep the same success/failure boundary rather than assuming the error policy is done just because the container changed.
Pitfalls
- Checking only from_chars’s ec and not ptr will accept partial input with an illegal suffix.
- After catch(const std::exception& e), using throw e; may slice derived exceptions; rethrow the current exception with throw;.
Run an example
Minimum C++17 · complete program · Download .cpp
#include <cassert>
#include <charconv>
#include <optional>
#include <stdexcept>
#include <string_view>
#include <system_error>
struct ParseError : std::runtime_error {
using std::runtime_error::runtime_error;
};
int parse_cents(std::string_view text) {
if (text.empty()) throw ParseError("empty amount");
int cents = 0;
const char* end = text.data() + text.size();
const auto result = std::from_chars(text.data(), end, cents);
if (result.ec != std::errc{} || result.ptr != end || cents < 0)
throw ParseError("invalid amount");
return cents;
}
int preserve_error(std::string_view text) {
try { return parse_cents(text); }
catch (const std::exception&) { throw; }
}
enum class Status { accepted, bad_amount };
struct Receipt { Status status; std::optional<int> cents; };
Receipt submit(std::string_view text) {
try { return {Status::accepted, parse_cents(text)}; }
catch (const ParseError&) { return {Status::bad_amount, std::nullopt}; }
}
int main() {
const auto good = submit("1250");
assert(good.status == Status::accepted && good.cents == 1250);
const auto bad = submit("12x");
assert(bad.status == Status::bad_amount && !bad.cents);
assert(!submit("9999999999999999999999999999999999999999").cents);
bool preserved = false;
try { (void)preserve_error(""); }
catch (const ParseError&) { preserved = true; }
assert(preserved);
}
Compile locally
g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread books-cpp-coding-standards.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
Make amount 0 a business error as well, without changing the low-level parser’s definition of a legal integer. What should be modified?
Show a reference answer
After submit calls parse_cents, save the result; if cents == 0, return {Status::bad_amount, std::nullopt}, otherwise return success. parse_cents("0") still yields 0 because zero is a legal integer; whether the business accepts zero is decided at the boundary. Add assertions that submit("0") fails and parse_cents("0") succeeds.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.