Object-Oriented Programming

Chapter 8
Exceptions, Defensive Programming, and Secure Coding

Bachelors Program in AI & ML
Object-Oriented Programming   •   Bachelor

Exceptions, Defensive Programming, and Secure Coding

Introduction

Reliable software does not merely produce correct results when everything goes well. It must also respond predictably when users provide invalid data, files disappear, networks fail, services return unexpected responses, or attackers deliberately exploit weaknesses. Object-oriented programming provides useful mechanisms for this purpose: exception classes, encapsulation, contracts, assertions, validation routines, and structured logging.

Exception handling and security are closely related. An unhandled exception can terminate a service; a poorly handled exception can reveal database credentials, internal paths, or sensitive user information. Similarly, weak input validation can cause both ordinary processing errors and security vulnerabilities. Defensive programming therefore treats abnormal conditions as expected design concerns rather than rare accidents.

This chapter explains how to design exception hierarchies, select appropriate error-handling strategies, validate inputs, enforce contracts, use assertions and logging, and avoid common vulnerabilities including injection, unsafe deserialization, and information leakage.

Learning Objectives

By the end of this chapter, students should be able to:

  • distinguish errors, exceptions, and validation failures;
  • design meaningful exception hierarchies;
  • choose between handling, propagating, translating, and recovering from exceptions;
  • apply preconditions, postconditions, invariants, and assertions appropriately;
  • validate and normalize input safely;
  • create useful logs without exposing confidential information;
  • recognize injection, unsafe deserialization, and information-leakage risks;
  • evaluate a defensive and secure coding design in an applied scenario.

1. Failures as Part of Software Design

A failure is an event in which a system cannot provide its intended behavior. Failures may arise from several sources:

1. Environmental failures: unavailable files, network timeouts, full disks, or database outages.

2. Input failures: malformed, missing, out-of-range, or unauthorized data.

3. Programming defects: incorrect assumptions, null references, invalid state transitions, or arithmetic errors.

4. Security attacks: malicious input designed to exploit implementation weaknesses.

5. Business-rule violations: legitimate data that conflicts with domain rules, such as attempting to withdraw more money than an account permits.

An exception is a language-supported representation of an abnormal condition. In many object-oriented languages, an exception is an object containing a type, message, and sometimes a cause or additional context. The type communicates what happened; the message assists diagnosis; the cause preserves the underlying failure.

A useful distinction is:

  • Validation failure: input does not satisfy an expected format or domain rule.
  • Recoverable operational failure: the program may retry, request another resource, or use a fallback.
  • Program defect: the implementation violates an assumption and requires correction.
  • Fatal failure: safe continuation is impossible.

These categories are not always absolute. A malformed request may be ordinary input failure at an API boundary but a programming defect if it reaches an internal method that promised to receive validated data.

2. Exception Hierarchies and Abstraction

2.1 Why exception hierarchies matter

An exception hierarchy groups related failures. A caller can handle a specific type when it knows the precise problem, or a general superclass when several failures require the same response. Good hierarchies reflect the problem domain rather than the implementation’s accidental details.

For example, an online bookstore might define:

```text

ApplicationException

├── ValidationException

│ ├── InvalidEmailException

│ └── InvalidQuantityException

├── AuthorizationException

├── ResourceException

│ ├── DatabaseUnavailableException

│ └── ExternalServiceException

└── OrderException

├── OrderNotFoundException

└── InsufficientInventoryException

```

This structure allows an API layer to translate all `ApplicationException` objects into controlled responses, while a service layer can distinguish `InsufficientInventoryException` from `DatabaseUnavailableException`.

2.2 Specificity and stable abstractions

Exception types should be:

  • Specific enough to support an appropriate response;
  • General enough to remain stable when implementation details change;
  • Meaningful to the caller, not merely descriptive of internal mechanics.

A data-access class should usually not expose vendor-specific exceptions directly to the business layer. Instead, it can translate them into an application-level exception such as `PersistenceException`. This is called exception translation and reduces coupling.

```python

class PersistenceException(Exception):

"""Application-level persistence failure."""

pass

def save_order(repository, order):

try:

repository.insert(order)

except DatabaseDriverError as exc:

raise PersistenceException("Order could not be saved") from exc

```

The public message is safe and stable, while the original exception is retained as a cause for internal diagnosis.

2.3 Checked and unchecked exceptions

Languages differ. Java distinguishes checked exceptions, which method signatures must declare or handle, from unchecked exceptions. Python and JavaScript do not enforce such declarations. Checked exceptions can make recoverable failures explicit, but excessive declarations may produce boilerplate and encourage unhelpful catching. Unchecked exceptions offer flexibility but place greater responsibility on documentation, testing, and design discipline.

Regardless of language, an exception should be documented when callers need to anticipate it. Documentation should describe the condition and the required response, not merely list implementation details.

3. Handling, Propagating, and Translating Exceptions

A robust strategy follows several principles.

Handle at the level that can act

A low-level function may know that a socket timed out, but a higher-level service may know whether retrying is safe. Catch an exception where a meaningful action is possible. Catching merely to print a message and continue often hides failure.

Preserve diagnostic context

When converting an exception, preserve the original cause where the language supports it. Avoid replacing a detailed cause with an unrelated message.

Catch narrowly

This is dangerous:

```python

try:

process_payment(request)

except Exception:

return "Payment failed"

```

It may conceal programming defects such as a `NameError` or a broken invariant. Prefer targeted exceptions, and use a broad boundary handler only at a carefully chosen application boundary where the error is logged and converted into a safe response.

Do not use exceptions for ordinary control flow

Exceptions are appropriate for unusual or invalid conditions. They are generally not appropriate for routine iteration, expected alternatives, or simple existence checks when a return value communicates the situation clearly.

Clean up resources

Files, locks, database connections, and network sockets require reliable cleanup. Use language-supported constructs such as Python’s `with`, Java’s try-with-resources, or C#’s `using`.

```python

with open(path, "r", encoding="utf-8") as file:

contents = file.read()

```

The resource is closed even if reading raises an exception.

Retry cautiously

Retries are appropriate for transient failures, such as temporary network unavailability, but not for invalid credentials or malformed requests. Retrying can amplify an outage or duplicate a non-idempotent operation. A sound retry policy defines a maximum number of attempts, delay or backoff, and an idempotency strategy.

4. Defensive Programming and Contracts

Defensive programming anticipates misuse, invalid state, environmental failure, and unexpected change. It does not mean accepting every possible condition silently. It means making assumptions explicit and failing safely when they are violated.

4.1 Contracts

A contract describes obligations between a caller and a component:

  • Preconditions: what must be true before an operation begins;
  • Postconditions: what the operation guarantees if it succeeds;
  • Invariants: properties that remain true for an object or system across public operations.

For a `BankAccount.withdraw(amount)` operation:

  • Preconditions may require that `amount` is positive.
  • A business rule may require sufficient available funds.
  • A postcondition may state that the balance decreases by exactly the withdrawn amount.
  • An invariant may require that the stored balance is never negative, if overdrafts are forbidden.

Contracts improve design because they identify responsibility. A public boundary should validate caller-controlled data. An internal method may rely on an established invariant, but that reliance must be documented and protected by tests.

4.2 Assertions

An assertion checks a condition believed to be true because of correct program logic.

```python

items = calculate_items(order)

assert items is not None

```

Assertions are suitable for programmer assumptions and internal invariants, not for security checks or user input validation. Many runtimes can disable assertions for production execution. If an authorization check is written only as an assertion, disabling assertions may remove a critical security control.

Use explicit validation for conditions that may be caused by external input:

```python

if quantity <= 0:

raise InvalidQuantityException("Quantity must be positive")

```

5. Input Validation and Safe Data Handling

Input validation should occur at trust boundaries: web requests, files, environment variables, message queues, command-line arguments, and third-party responses.

5.1 A validation process

A practical process is:

1. Identify the source and trust level of the data.

2. Establish the expected type, range, length, format, and encoding.

3. Parse into a structured type rather than operating on raw text.

4. Reject invalid data early and consistently.

5. Normalize only when the transformation is well-defined.

6. Apply authorization after identifying the relevant resource.

7. Validate again when data crosses a new trust boundary.

Validation should be based on allowlists whenever possible. For example, an order status may be restricted to `pending`, `paid`, `shipped`, or `cancelled`, rather than attempting to blacklist every unwanted string.

5.2 Validation is not sanitization

Validation determines whether data is acceptable for a particular purpose. Sanitization or encoding transforms data so that it is safe in a particular output context. HTML escaping, SQL parameter binding, and shell argument escaping are context-specific. A string safe in HTML text may not be safe in a JavaScript string or SQL query.

5.3 Canonicalization

Attackers may exploit multiple representations of the same data, such as encoded paths, differing Unicode forms, or case variations. A system should canonicalize data before security decisions where appropriate, then validate the canonical representation. File access controls, for example, should prevent traversal such as `../` and should resolve paths within an approved directory.

6. Common Secure-Coding Risks

6.1 Injection

Injection occurs when untrusted data is interpreted as code or commands. Common forms include SQL, command, LDAP, template, and cross-site scripting injection.

Unsafe SQL construction:

```python

query = "SELECT * FROM users WHERE name = '" + username + "'"

```

If `username` contains SQL syntax, it may alter the query. Parameterized queries separate code from data:

```python

cursor.execute(

"SELECT * FROM users WHERE name = ?",

(username,)

)

```

The same principle applies to shell commands: prefer an API that accepts an argument list rather than concatenating a command string. Output encoding must also match the destination context.

6.2 Unsafe deserialization

Serialization converts object state into a transferable representation; deserialization reconstructs it. Deserializing data from an untrusted source can be dangerous when the format permits arbitrary object creation, method execution, or resource consumption.

Safer practices include:

  • use data-only formats with restricted schemas, such as carefully validated JSON;
  • avoid language-native object serialization for untrusted data;
  • permit only explicitly recognized types;
  • enforce size, nesting, and complexity limits;
  • authenticate the data when integrity is required;
  • validate the reconstructed object before use.

A digital signature or message authentication code can establish integrity, but it does not automatically make a dangerous serialization format safe. Confidentiality and integrity are different properties.

6.3 Information leakage

Error responses, logs, stack traces, timing behavior, and identifiers can reveal sensitive information. Production clients should receive a general error and a correlation identifier, while detailed diagnostics remain in protected logs.

Avoid exposing:

  • passwords, access tokens, session cookies, and cryptographic keys;
  • full payment-card or government-identification numbers;
  • database connection strings;
  • stack traces and internal file paths;
  • whether a particular account exists, when that fact is sensitive.

Messages should be useful without becoming an information oracle. For example, “Invalid username or password” is often safer than identifying which credential was incorrect.

6.4 Authentication and authorization failures

Authentication establishes identity; authorization determines permitted actions. A valid identity does not imply permission to access every object. Authorization must be enforced server-side, on every relevant operation, and against the resource actually being accessed. Client-side controls are useful for usability but cannot be trusted as security boundaries.

7. Logging, Monitoring, and Observability

Logging supports debugging, incident response, and operational analysis. A useful event generally includes a timestamp, severity, event type, correlation or request identifier, component, and safe contextual fields.

Logging levels often include:

  • DEBUG: detailed development diagnostics;
  • INFO: significant normal operations;
  • WARNING: unusual conditions that do not yet prevent operation;
  • ERROR: an operation failed;
  • CRITICAL: immediate attention is required.

Log the exception with its stack trace at the appropriate internal boundary, but do not duplicate the same exception at every layer. Excessive duplication obscures the root event. Logs must be protected against unauthorized access and tampering, and retention should follow organizational and legal requirements.

Never record secrets merely because they are convenient for debugging. Redact sensitive fields, use structured logging, restrict access, and consider log injection: untrusted input containing newline characters or control sequences can forge misleading log entries unless safely encoded.

8. An Exception and Security Boundary Model

A useful architecture separates responsibilities:

1. Input boundary: parse, validate, authenticate, and reject malformed requests.

2. Domain layer: enforce business rules and invariants; raise meaningful domain exceptions.

3. Infrastructure layer: translate driver and operating-system failures into stable application exceptions.

4. Application boundary: map known failures to safe client responses, log unexpected failures, and preserve correlation identifiers.

5. Monitoring layer: aggregate events, detect patterns, and alert on abnormal behavior.

This model avoids two common extremes: exposing internal exceptions directly to users and swallowing exceptions so completely that operators cannot diagnose failures.

9. Worked Example: Secure Order Processing

Consider a service that accepts an order containing a customer identifier, product identifier, quantity, and payment token.

A defensible sequence is:

1. Parse the request into a data-transfer object.

2. Validate required fields, types, maximum lengths, and positive quantity.

3. Authenticate the caller.

4. Authorize whether the caller may place orders for the specified account.

5. Load the product through a parameterized query.

6. Check inventory and price using trusted server-side data; never trust a client-supplied total.

7. Call the payment provider using a bounded timeout and an idempotency key.

8. Store the order within a transaction, handling persistence failures through an application-level exception.

9. Return a response containing an order reference, not internal database details.

10. Log a correlation identifier, outcome, duration, and safe identifiers, but not the payment token.

Possible domain exceptions include `InvalidQuantityException`, `UnauthorizedOrderException`, `InsufficientInventoryException`, and `PaymentDeclinedException`. The API layer may map them respectively to controlled client responses, while unexpected exceptions produce a generic server-error response and an internal alert.

A subtle issue is retrying payment. If the client repeats a request after a timeout, the service must avoid charging twice. An idempotency key allows the payment provider or service to recognize a repeated logical operation. This illustrates why reliability and security cannot be designed independently.

10. Illustrative Case Study: University Model-Training Platform

Background

A university provides a platform on which students upload datasets and run machine-learning training jobs. The platform stores uploaded files, invokes training workers, and displays job status.

Problem

An initial implementation accepted archive files, extracted them using filenames supplied by users, and deserialized a configuration object stored in each submission. Errors returned stack traces to the browser. Logs included access tokens to simplify debugging.

Analysis

The design contained several risks:

  • archive path traversal could write files outside the student’s workspace;
  • unsafe deserialization could execute attacker-controlled behavior;
  • stack traces disclosed server paths and library versions;
  • tokens in logs increased the impact of log compromise;
  • unrestricted archive size enabled resource exhaustion;
  • authorization checked the project identifier supplied by the browser but did not verify ownership server-side.

Approach and solution

The revised design used a validated JSON schema for configuration, permitted only expected scalar fields, and imposed limits on file size, archive entries, nesting, and decompression expansion. Each archive path was normalized and checked to remain within a dedicated workspace. The service used a safe extraction library and ran training jobs in isolated workers with restricted permissions.

The API performed authentication and server-side project authorization before reading or writing data. Internal exceptions were translated into stable application exceptions. Clients received generic error messages with correlation identifiers. Logs recorded job identifiers and failure categories, while tokens and raw uploaded content were excluded. Assertions checked internal worker invariants, but explicit validation enforced all security-critical conditions.

Outcome

The platform gained clearer failure behavior, safer isolation, and more actionable monitoring. Some functionality became less flexible because arbitrary serialized Python objects were no longer accepted. This was an intentional trade-off: extensibility was achieved through a documented data schema rather than executable object reconstruction.

Learning points

Secure coding is not a collection of isolated patches. It requires trust-boundary analysis, controlled data formats, least privilege, resource limits, safe error handling, and operational visibility. AI and machine-learning systems add risks because uploaded datasets, configuration files, models, and generated commands may all be attacker-controlled inputs.

11. Challenges, Trade-offs, and Governance

Defensive mechanisms have costs. Strict validation can reject legitimate variation; verbose logging can increase storage and privacy exposure; retries can improve availability but duplicate operations; detailed client errors aid usability but reveal information. Design decisions should therefore be risk-based and documented.

Organizations should establish secure coding standards, code review practices, dependency-management procedures, vulnerability disclosure processes, and incident-response plans. Privacy regulations may restrict collection and retention of personal data in logs. Security controls should be tested through unit tests, integration tests, negative tests, static analysis, dependency scanning, and—where appropriate—penetration testing.

Contemporary development increasingly uses automated scanners, software composition analysis, secret detection, runtime monitoring, and policy-as-code. These tools assist developers but do not replace threat modeling or human review. A scanner may identify a potentially unsafe deserialization call, but engineers must determine the trust boundary and select an appropriate redesign.

Best-Practice Checklist

  • Define an exception taxonomy aligned with application responsibilities.
  • Catch exceptions only when meaningful recovery, translation, or safe reporting is possible.
  • Preserve causes and diagnostic context internally.
  • Validate data at every relevant trust boundary.
  • Prefer allowlists, typed parsing, and explicit size limits.
  • Use parameterized queries and context-appropriate output encoding.
  • Avoid unsafe deserialization of untrusted data.
  • Keep authorization checks on the server and close to protected operations.
  • Use assertions for internal assumptions, never as the sole security control.
  • Return safe external errors and detailed, protected internal diagnostics.
  • Redact secrets and protect logs.
  • Design retries with bounded attempts, backoff, and idempotency.
  • Test failure paths as deliberately as successful paths.

Chapter Summary

Exceptions provide a structured way to represent abnormal conditions, but effective error handling depends on choosing meaningful types, handling failures at the right architectural level, translating implementation-specific errors, preserving causes, and cleaning up resources. Defensive programming makes assumptions explicit through validation, contracts, invariants, and carefully applied assertions.

Secure coding extends these ideas to hostile inputs and operational environments. Parameterized interfaces prevent injection, restricted data-only formats reduce deserialization risk, canonicalization and resource limits defend against manipulation and exhaustion, and carefully designed messages and logs reduce information leakage. A sound architecture separates input validation, domain rules, infrastructure translation, and application-level error reporting. Reliable and secure software treats failure analysis, privacy, authorization, monitoring, and recovery as core design responsibilities.

Key Terms

Exception: An object or mechanism representing an abnormal runtime condition.

Exception hierarchy: A structured set of related exception types connected by inheritance.

Exception translation: Converting a low-level failure into a stable, higher-level application exception.

Validation: Determining whether data satisfies required type, format, range, and business rules.

Defensive programming: Designing software to respond safely to invalid input, misuse, and environmental failure.

Precondition: A condition that must hold before an operation begins.

Postcondition: A condition guaranteed after successful completion.

Invariant: A property that remains true across valid object or system states.

Assertion: A check of an internal assumption or invariant.

Trust boundary: A point where data enters or leaves a security domain.

Canonicalization: Converting data into a standard representation before evaluation.

Injection: Causing untrusted data to be interpreted as commands or code.

Parameterized query: A database operation that separates query structure from values.

Deserialization: Reconstructing data or objects from a serialized representation.

Information leakage: Unintended disclosure of sensitive data or implementation details.

Authentication: Establishing the identity of a subject.

Authorization: Determining what an authenticated subject may do.

Idempotency: A property whereby repeating a logical operation does not create additional unintended effects.

Correlation identifier: A value used to connect logs and events belonging to one request or transaction.

Least privilege: Giving users and components only the permissions they require.

Review Questions

Short Answer Questions

1. What is the difference between validation failure and a programming defect?

2. Why should exception types reflect application-level meaning?

3. When is an assertion appropriate, and when is it inappropriate?

4. Why are parameterized queries safer than concatenated SQL strings?

5. Name three types of information that should not normally appear in logs.

Long Answer Questions

1. Explain how exception translation reduces coupling between infrastructure and business layers.

2. Discuss preconditions, postconditions, and invariants using a banking or inventory example.

3. Explain the relationship between validation, canonicalization, and output encoding.

4. Describe a secure error-reporting and logging strategy for a web application.

5. Analyze the risks of unsafe deserialization and propose safer alternatives.

Analytical / Application Questions

1. Design an exception hierarchy for a hospital appointment system and explain where each exception should be handled.

2. A service retries a payment request after every timeout. Analyze the reliability and security risks and propose a safer design.

3. Review a file-upload service that accepts archives and displays raw stack traces. Identify at least five vulnerabilities and design a remediation plan.

Multiple Choice Questions with Answers

Question 1

Which exception-handling practice best preserves abstraction between infrastructure and business code?

A. Expose every database-driver exception directly

B. Translate low-level failures into meaningful application exceptions

C. Catch every exception and ignore it

D. Replace all exceptions with a Boolean value

Correct Answer: B. Translate low-level failures into meaningful application exceptions

Question 2

Which condition is most appropriate for an assertion?

A. A user-entered password has the required length

B. A user is authorized to access an account

C. An internal algorithm invariant holds after a calculation

D. An uploaded file is below the permitted size

Correct Answer: C. An internal algorithm invariant holds after a calculation

Question 3

What is the primary security advantage of a parameterized SQL query?

A. It automatically encrypts the database

B. It separates SQL structure from user-supplied values

C. It guarantees that the user is authorized

D. It prevents all denial-of-service attacks

Correct Answer: B. It separates SQL structure from user-supplied values

Question 4

Which practice is safest when processing untrusted serialized data?

A. Deserialize it using the language’s native object mechanism

B. Permit every class but log failures

C. Use a restricted data-only format and validate its schema

D. Disable exception handling during deserialization

Correct Answer: C. Use a restricted data-only format and validate its schema

Question 5

Where should an exception generally be handled?

A. At the lowest possible function regardless of available recovery

B. Where meaningful recovery, translation, or safe reporting is possible

C. Only inside the user interface

D. Never; all exceptions should terminate the process

Correct Answer: B. Where meaningful recovery, translation, or safe reporting is possible

Question 6

Which item is most likely to cause information leakage?

A. A generic client error with a correlation identifier

B. A protected internal log containing an event category

C. A production response containing a full stack trace and database path

D. A redacted log containing request duration

Correct Answer: C. A production response containing a full stack trace and database path

Question 7

What is the main purpose of server-side authorization?

A. To improve the visual design of a client application

B. To verify that the authenticated subject may perform the requested action

C. To convert text into a canonical encoding

D. To retry failed network requests

Correct Answer: B. To verify that the authenticated subject may perform the requested action

Question 8

Why can unrestricted archive extraction be dangerous?

A. It always causes SQL injection

B. It may enable path traversal or resource exhaustion

C. It prevents all authentication attempts

D. It makes assertions unavailable

Correct Answer: B. It may enable path traversal or resource exhaustion

Question 9

Which feature is especially important when retrying a payment after a timeout?

A. A larger stack trace

B. An idempotency strategy

C. A weaker validation policy

D. A client-side authorization check only

Correct Answer: B. An idempotency strategy

Question 10

Which statement correctly distinguishes validation from sanitization?

A. Validation checks acceptability; sanitization or encoding makes data suitable for a context

B. Validation and sanitization are identical in every context

C. Sanitization proves that a user is authorized

D. Validation is needed only for database input

Correct Answer: A. Validation checks acceptability; sanitization or encoding makes data suitable for a context

Object-Oriented Programming
Chapter 8 of 15