Object Oriented Programming using 'C++

Chapter 9
Exceptions, Contracts and Defensive Design

Post Graduate Program in Computer Science Engineering
Object Oriented Programming using 'C++   •   Post Graduate

9. Exceptions, Contracts and Defensive Design

Introduction and Context

A useful object-oriented program must do more than produce correct results for ideal inputs. It must also behave predictably when files are missing, network services fail, memory is exhausted, input violates assumptions, or a dependent component returns an unexpected value. Such events are not merely programming inconveniences; they are part of the system’s observable behaviour and must be designed explicitly.

C++ provides several mechanisms for expressing and managing failure. Exceptions are appropriate for abnormal conditions that cannot be handled locally. Assertions document and check programmer assumptions. Contract-style preconditions, postconditions, and invariants describe what an interface promises. `std::optional` represents the legitimate absence of a value, while expected-style results—such as `std::expected` in C++23 or established library equivalents—represent either a value or a structured error. Defensive design combines these mechanisms with resource-management techniques, validation, clear ownership, and controlled recovery.

The central problem is not simply how to throw or catch an exception. It is how to classify failures, assign responsibility, preserve program invariants, prevent partial corruption, and expose a reliable interface to clients. This chapter develops that perspective and relates it to object-oriented design, software architecture, and professional engineering practice.

Learning Objectives

After studying this chapter, a student should be able to:

1. Distinguish errors, exceptions, contract violations, and expected absence of values.

2. Design an error taxonomy and select an appropriate reporting mechanism.

3. Explain basic exception guarantees and apply RAII to exception-safe resource management.

4. Use assertions, preconditions, postconditions, and class invariants responsibly.

5. Compare exceptions, `std::optional`, and expected-style results.

6. Analyse recovery, propagation, translation, logging, and failure-containment strategies.

7. Identify common exception-safety defects in C++ programs.

8. Evaluate defensive designs in technical and organisational contexts.

Failure as Part of an Interface

Failure is an interaction between a program and its environment. A malformed command-line argument, for example, may be invalid input at an application boundary but a programmer error if it reaches an internal function whose precondition explicitly excludes that value. The same event may therefore require different treatment at different layers.

A practical taxonomy has four broad categories. Environmental failures arise from external conditions: unavailable files, timeouts, permission errors, exhausted storage, or service failures. Input failures occur when data does not satisfy a format or domain rule. Programming errors violate assumptions established by the implementation, such as dereferencing an invalid iterator or calling a function with an impossible state. Business or domain outcomes are conditions such as “account not found,” “insufficient credit,” or “no search result”; these may be normal outcomes rather than exceptional events.

The taxonomy influences the interface. A missing optional configuration value may be represented by `std::optional`. A rejected business command may be represented by an expected-style result carrying a domain error. A failure deep in a file or database operation may be reported through an exception when the caller cannot reasonably handle it at the point where it occurs. An invalid internal invariant should normally be detected with an assertion during development and corrected in the implementation, rather than silently converted into ordinary user input failure.

A useful design question is: Who can make a meaningful decision about this condition? The component that detects a failure is not always the component that can recover from it. Low-level code should preserve useful information and avoid pretending to recover. Higher-level code may retry, choose a fallback, notify a user, roll back a transaction, or terminate a request.

Exceptions in C++

An exception transfers control from the point where a failure is detected to a matching handler. A typical operation is expressed with `throw`, `try`, and `catch`:

```cpp

#include <stdexcept>

#include <string>

int parse_port(const std::string& text) {

int port = std::stoi(text);

if (port < 1 || port > 65535)

throw std::out_of_range("port outside valid range");

return port;

}

void configure(const std::string& text) {

try {

int port = parse_port(text);

// use port

} catch (const std::invalid_argument& e) {

// report malformed text

} catch (const std::out_of_range& e) {

// report an invalid numeric range

}

}

```

During stack unwinding, automatic objects whose lifetimes end are destroyed. This behaviour is fundamental: a function may acquire a resource in a local object, throw later, and still release the resource if the object follows RAII. Handlers should generally catch exceptions by `const` reference to avoid slicing and unnecessary copying. Catching `...` is appropriate only at carefully chosen boundaries, such as a thread entry point or process-level request handler, where the program must prevent an exception from escaping further.

Standard exception classes in `<stdexcept>` include `invalid_argument`, `out_of_range`, `logic_error`, `runtime_error`, and `system_error`. A project may define domain-specific types when clients need to distinguish meaningful categories. Exceptions should carry actionable context, but sensitive data such as passwords, tokens, or personal information must not be placed in messages or logs.

An exception should normally be thrown by value and caught by reference. A handler should either recover, translate the error while preserving its cause, add context and rethrow, or terminate at a deliberate boundary. Catching an exception merely to print a message and continue is dangerous when the affected object may be partially modified.

Exception-Safety Guarantees

Exception safety describes what remains true when an operation fails. Three classical guarantees are particularly useful.

The basic guarantee states that no resource leaks occur and that object invariants remain valid, although observable state may have changed. The strong guarantee promises transactional behaviour: either the operation succeeds completely or the observable state remains unchanged. The no-throw guarantee promises that the operation will not throw. Destructors, swap operations used by standard containers, and carefully designed move operations often need this guarantee.

These guarantees are not merely labels. They help clients decide whether retrying is safe and whether failure can leave a system usable. Consider a vector-like operation that inserts several elements. If an exception occurs after some elements have been moved, the operation may provide only the basic guarantee. A transaction update, by contrast, often requires the strong guarantee so that a failed update does not leave half of the new configuration installed.

The copy-and-swap idiom is a common way to obtain the strong guarantee:

```cpp

class Configuration {

public:

Configuration(const Configuration&) = default;

Configuration& operator=(const Configuration& other) {

Configuration temporary(other); // may throw; this remains unchanged

swap(*this, temporary); // designed not to throw

return *this;

}

friend void swap(Configuration& a, Configuration& b) noexcept {

using std::swap;

swap(a.values_, b.values_);

}

private:

std::vector<int> values_;

};

```

The temporary is fully constructed before the target object changes. If copying fails, the original remains intact. Once `swap` begins, it must be non-throwing. Modern designs may use move operations and standard containers to achieve the same effect, but the underlying principle remains: complete risky work before committing visible state.

RAII and Failure Containment

Resource Acquisition Is Initialization binds ownership to object lifetime. Files, locks, sockets, transactions, memory, and temporary system handles should be represented by types whose destructors release them. This is safer than manually pairing `acquire()` and `release()` across many return and exception paths.

```cpp

void process_file(const std::string& name) {

std::ifstream input(name); // ownership begins here

if (!input)

throw std::runtime_error("cannot open input file");

std::string line;

while (std::getline(input, line)) {

// processing may throw; input is still closed automatically

}

}

```

RAII does not guarantee logical correctness. A transaction guard may release a lock but still need to roll back an uncommitted transaction. A destructor should usually not throw, because it may execute during stack unwinding; a second exception during unwinding can call `std::terminate`. Cleanup failures should be recorded, handled explicitly, or exposed through a separate operation rather than thrown from a destructor.

Failure containment means limiting the region affected by an error. A request handler can isolate one failed request from other requests. A plugin boundary can prevent third-party exceptions from crossing an ABI or ownership boundary. A worker thread should catch exceptions before its entry function ends, since an uncaught exception in a thread leads to termination. At each boundary, the system should translate internal detail into a stable external error model.

Contracts: Preconditions, Postconditions and Invariants

A contract states the obligations of an operation and the guarantees it provides. A precondition describes what a caller must establish. A postcondition describes what a successful operation guarantees. A class invariant describes a property that must hold for every observable valid state of an object.

For a bank account, a precondition for withdrawal may be that the amount is positive. A postcondition may be that the balance has decreased by exactly that amount. An invariant may be that the balance is never negative, if overdrafts are not permitted. Contracts improve design because they make responsibilities explicit rather than scattering assumptions through implementation code.

C++ does not, in all language versions, provide a universal built-in contract system. Contracts can be expressed through documentation, assertions, validation functions, types, and libraries. C++23’s `assert` remains a diagnostic facility rather than a complete runtime contract framework. An assertion should check a condition that indicates a programming or implementation defect, not ordinary data supplied by an untrusted user.

```cpp

class Percentage {

public:

explicit Percentage(int value) : value_(value) {

if (value < 0 || value > 100)

throw std::invalid_argument("percentage out of range");

}

int value() const noexcept { return value_; }

private:

int value_;

};

```

Here, construction validates external or caller-provided data. An assertion might be suitable inside a private method that is reachable only after construction and whose invariant should make an impossible state detectable during testing. Assertions can be disabled in some release configurations, so they must not be the sole protection for security checks, authorization, memory safety, or user input validation.

Contracts must also be realistic. A postcondition that promises ordering, timing, or exact error text without a genuine need unnecessarily restricts future implementations. Strong contracts improve substitutability, but over-specification makes evolution difficult.

`std::optional` and Expected-Style Results

`std::optional<T>` models a value that may legitimately be absent. It is appropriate when absence carries little additional diagnostic information and is not exceptional for the operation:

```cpp

std::optional<User> find_user(UserId id);

if (auto user = find_user(id)) {

display(*user);

} else {

display_not_found();

}

```

An optional should not be used to conceal operational failures. If a database connection failed, returning an empty optional makes “not found” indistinguishable from “service unavailable.” The representation has erased important information.

An expected-style result represents either a successful value or an error. In C++23, `std::expected<T, E>` supports this model. A simplified example is:

```cpp

std::expected<User, UserError> load_user(UserId id) {

if (!database_available())

return std::unexpected(UserError::Unavailable);

if (!exists(id))

return std::unexpected(UserError::NotFound);

return read_user(id);

}

```

This style makes recoverable failure visible in the function’s type and is often valuable in parsers, embedded systems, high-throughput services, and APIs where callers are expected to inspect every result. It may produce more explicit propagation code, but that explicitness can be a strength. Exceptions, by contrast, keep the success path concise and allow failure to cross several layers until a suitable handler is reached.

The choice depends on semantics, not fashion. Use optional for ordinary absence, expected-style results for expected alternatives requiring structured handling, and exceptions for failures that are exceptional relative to the operation and best handled at a different level. A project should define consistent conventions rather than allowing each developer to choose arbitrarily.

Propagation, Translation and Recovery

An error should be propagated until a component has both the authority and information to act. A lower-level library may throw a `system_error`; an application service may translate it into `StorageUnavailable`; a user-interface layer may display a retry option. Translation should preserve causality where possible, using nested exceptions or an error object containing the original category and context.

Recovery strategies include retrying, using a fallback, rolling back, compensating for a completed action, rejecting the request, isolating the failed component, and terminating. Retry is safe only when the operation is idempotent or protected by an appropriate request identifier. Blind retries can multiply payments, duplicate messages, or overload an already failing service. Timeouts, bounded retry counts, exponential backoff, and circuit breakers are architectural forms of defensive design.

Logging must be performed at the right boundary. Logging the same exception at every layer creates noise; logging nowhere loses evidence. A useful policy is to add context when rethrowing and record the failure once at the boundary responsible for the final response. Logs should support correlation across services while respecting privacy, retention, and access-control policies.

Illustrative Case Study: Configuration Reload in a Service

Background

Consider a fictional payment gateway that reads routing rules from a configuration file. The service must continue handling existing requests while a new configuration is validated and installed.

Situation and Problem

An initial implementation clears the active rule map, reads the new file, and inserts rules one by one. If parsing fails halfway through, the service remains running but has only part of its routing configuration. Some requests are routed incorrectly, and the defect is difficult to diagnose.

Analysis

The operation needs the strong guarantee: a failed reload must leave the previous valid configuration unchanged. File absence and malformed syntax are environmental or input failures that should be reported to the administrative caller. An internal violation such as a duplicate rule after validation indicates a defect in the parser or its assumptions and should not be silently ignored. The active configuration is shared, so replacement must also be synchronized.

Approach and Solution

The service parses the file into a temporary `Configuration` object. Its constructor and parsing functions validate syntax, ranges, duplicate keys, and semantic relationships. The temporary object is complete before publication. A non-throwing swap under a short lock replaces the active configuration atomically from the service’s perspective. A failed parse leaves the original object untouched. The file handle is managed by RAII, and the administrative API returns an expected-style result distinguishing `NotFound`, `PermissionDenied`, and `InvalidConfiguration`. Unexpected exceptions are caught at the request boundary, logged with a correlation identifier, and converted into a generic internal-error response.

Outcome

Users continue receiving requests using the last known good configuration. Administrators receive actionable diagnostics, while internal implementation details are not exposed externally. The design supports safe retry after correction and prevents partial state publication.

Learning Points

The case demonstrates that exception safety is a state-management property, not merely a matter of catch blocks. Temporary construction, validation before commitment, RAII, explicit error categories, and a carefully chosen publication boundary work together. It also shows why recovery must be designed around idempotence and state integrity.

Common Mistakes, Risks and Limitations

A frequent mistake is catching `std::exception` too early and converting every failure into a generic message. This prevents higher layers from recovering appropriately. The opposite mistake—allowing implementation-specific exceptions to cross a public API—couples clients to unstable details. Another defect is throwing from destructors or cleanup functions, which can terminate the program during stack unwinding.

Exception specifications should be meaningful. Marking a function `noexcept` when it can actually throw causes termination rather than graceful propagation. Failing to mark genuinely non-throwing move constructors can make standard containers choose copying, reducing performance or changing exception guarantees. Error messages should not be treated as a stable machine-readable protocol; use error codes or typed categories for programmatic decisions.

Defensive validation also has limits. Excessive checks can duplicate invariants and make code difficult to maintain. Assertions may be disabled. Input validation cannot replace authorization, concurrency control, resource limits, or threat modelling. Error handling itself may reveal sensitive information, create denial-of-service opportunities through expensive retries, or cause inconsistent distributed state.

Professional and Governance Considerations

Production systems require an agreed failure policy: which conditions are retryable, which are audited, which terminate a request, and which trigger escalation. Security-sensitive applications should fail closed for authorization decisions, avoid exposing stack traces, and separate diagnostic logs from user-facing messages. Financial, medical, and industrial systems may require traceability of rejected operations and evidence that invariants were preserved.

Code review should examine not only the successful path but also ownership, rollback, exception guarantees, logging, concurrency, and behaviour under repeated failure. Testing should include malformed input, unavailable dependencies, allocation failure where feasible, interrupted operations, and exceptions from callbacks. Fault injection and property-based testing can reveal assumptions that ordinary unit tests miss.

Emerging Directions

Modern C++ increasingly combines zero-overhead abstractions with explicit error types. `std::expected`, ranges-based validation, strong value types, and static analysis tools encourage errors to be represented near their source. Sanitizers, compiler warnings, fuzzers, contract-oriented libraries, and observability platforms complement language mechanisms. In asynchronous systems, error propagation must also account for futures, coroutines, cancellation, and partial completion. A coroutine may return an expected-style result while cancellation is represented separately, because cancellation is often a control decision rather than a fault.

Artificial intelligence can assist with log clustering, anomaly detection, and test generation, but automated suggestions do not establish correct contracts. Engineers must verify whether a proposed recovery is safe, whether sensitive data is exposed, and whether a generated catch block masks a serious defect.

Best Practices

1. Classify failures before choosing a mechanism.

2. Make ownership explicit through RAII and value-oriented types.

3. Preserve object invariants on every exit path.

4. Prefer the strong guarantee for transactional replacement operations.

5. Catch by `const` reference and catch at meaningful boundaries.

6. Do not use optional when it would erase important failure information.

7. Use assertions for programmer assumptions, not untrusted input validation.

8. Make retries bounded, observable, and safe for the operation’s idempotence properties.

9. Translate errors at architectural boundaries while preserving useful causes.

10. Test failure paths as deliberately as success paths.

Chapter Summary

Exception-safe C++ design begins with a taxonomy of failures and a clear allocation of responsibility. Exceptions support non-local propagation, while `std::optional` represents ordinary absence and expected-style results represent explicit success-or-error alternatives. RAII prevents resource leaks, and exception-safety guarantees describe the state preserved after failure. Contracts express preconditions, postconditions, and invariants; assertions help expose programming defects but cannot replace validation or security controls. Reliable systems validate before committing state, isolate failures at boundaries, recover only when recovery is justified, and preserve diagnostic context without exposing sensitive information. Defensive design is therefore a coordinated practice involving language features, object design, architecture, testing, governance, and professional judgement.

Key Terms

Exception: A mechanism for transferring control to a handler when an operation cannot complete normally.

Stack unwinding: Destruction of automatic objects as control leaves nested scopes during exception propagation.

RAII: Resource Acquisition Is Initialization; binding resource ownership to object lifetime.

Basic guarantee: Failure causes no leaks and preserves invariants, though observable state may change.

Strong guarantee: An operation either succeeds or leaves observable state unchanged.

No-throw guarantee: An operation is guaranteed not to throw an exception.

Precondition: A requirement the caller must satisfy before an operation begins.

Postcondition: A property guaranteed after successful completion.

Invariant: A property that remains true for every valid observable object state.

Assertion: A diagnostic check for a condition expected to hold because of program logic.

Exception translation: Converting a lower-level failure into a domain-appropriate error representation.

Failure containment: Limiting the effects of an error to a controlled component or boundary.

`std::optional`: A type representing either a value or legitimate absence.

Expected-style result: A type representing either a successful value or a structured error.

Idempotence: The property that repeating an operation has the same intended effect as performing it once.

Rollback: Restoring state after an operation cannot complete successfully.

Commit: The point at which prepared changes become visible as authoritative state.

Defensive design: Designing software to remain safe, diagnosable, and predictable under invalid conditions.

Review Questions

Short Answer Questions

1. Distinguish an environmental failure from a programming error.

2. What is the purpose of the strong exception-safety guarantee?

3. Why should destructors generally not throw?

4. When is `std::optional` more appropriate than an exception?

5. State the difference between a precondition and a class invariant.

Long Answer Questions

1. Explain RAII, stack unwinding, and their contribution to exception-safe resource management.

2. Compare exceptions and expected-style results in terms of control flow, visibility, and suitability.

3. Discuss the basic, strong, and no-throw guarantees with suitable C++ examples.

4. Explain how contracts and assertions support defensive object-oriented design.

5. Describe how errors should be propagated, translated, logged, and handled across architectural boundaries.

Analytical / Application Questions

1. A configuration reload clears the active configuration before parsing a new file. Analyse the possible failure states and redesign the operation to provide the strong guarantee.

2. A payment service retries a timed-out request three times. Identify the risks and specify the conditions under which retry is safe.

3. Design an error model for a user lookup API that must distinguish successful lookup, user absence, database unavailability, and malformed identifiers. Justify whether each outcome should use optional, expected-style results, exceptions, or validation.

22. Multiple Choice Questions with Answers

Question 1

Which situation is most appropriately represented by std::optional?

A. A database connection failed and the cause must be reported

B. A search completed successfully but found no matching object

C. An object invariant was violated internally

D. A file permission error occurred during an update

Correct Answer: B. A search completed successfully but found no matching object

Question 2

What does the strong exception-safety guarantee provide?

A. The operation never allocates memory

B. The operation cannot be interrupted

C. Failure leaves observable state unchanged

D. All exceptions are converted into error codes

Correct Answer: C. Failure leaves observable state unchanged

Question 3

Why is RAII important during exception propagation?

A. It prevents all exceptions from being thrown

B. It ensures owned resources are released as objects are destroyed

C. It disables stack unwinding

D. It makes every function noexcept

Correct Answer: B. It ensures owned resources are released as objects are destroyed

Question 4

Which condition is generally most suitable for an assertion?

A. A user enters an invalid password

B. A file is absent

C. An internal invariant that should be impossible is false

D. A customer has insufficient credit

Correct Answer: C. An internal invariant that should be impossible is false

Question 5

Why can returning an empty optional for database failure be defective?

A. Optional cannot contain objects

B. It confuses service failure with legitimate absence

C. Optional always throws on access

D. It prevents compilation in C++23

Correct Answer: B. It confuses service failure with legitimate absence

Question 6

What is the safest design for an exception-prone configuration reload requiring the strong guarantee?

A. Clear the active configuration and rebuild it incrementally

B. Modify the active configuration while parsing

C. Build and validate a temporary configuration, then swap it into place

D. Catch every exception and continue with partial data

Correct Answer: C. Build and validate a temporary configuration, then swap it into place

Question 7

Why should a retry policy consider idempotence?

A. Non-idempotent operations may produce duplicate effects

B. Idempotent operations cannot time out

C. Only idempotent operations can use RAII

D. Retries automatically preserve class invariants

Correct Answer: A. Non-idempotent operations may produce duplicate effects

Question 8

How should a public boundary usually handle an implementation-specific lower-level exception?

A. Expose its exact type permanently

B. Suppress it without recording context

C. Translate it into a stable domain error while preserving useful cause information

D. Convert it unconditionally into success

Correct Answer: C. Translate it into a stable domain error while preserving useful cause information

Question 9

Which statement about throwing destructors is correct?

A. They are required for strong exception safety

B. They can cause termination if another exception is already unwinding

C. They prevent resource cleanup

D. They are equivalent to assertions

Correct Answer: B. They can cause termination if another exception is already unwinding

Question 10

Which result model best represents a parser that can return either a value or one of several expected, structured errors?

A. A raw pointer with nullptr for every failure

B. std::optional with all errors encoded as absence

C. An expected-style result containing a value or error type

D. An assertion followed by undefined behaviour

Correct Answer: C. An expected-style result containing a value or error type

Object Oriented Programming using 'C++
Chapter 9 of 15