Programming Fundamentals (Python/C++)

Chapter 9
Errors, Exceptions, Debugging, and Defensive Programming

Bachelors Program in Full Stack Development
Programming Fundamentals (Python/C++)   •   Bachelor

Errors, Exceptions, Debugging, and Defensive Programming

Introduction

Software rarely fails for only one reason. A program may be rejected before execution because its syntax is invalid, terminate while running because an operation is impossible, produce an incorrect result despite apparently normal execution, or implement an interpretation of the requirements that is technically coherent but semantically wrong. Professional programming therefore requires more than writing code that works on a successful path. It requires the systematic prevention, detection, diagnosis, communication, and recovery of failure.

This chapter develops a practical and conceptual framework for error handling in Python and C++. It distinguishes major categories of errors, explains the relationship between exceptions and program control flow, and introduces assertions, logging, tracing, breakpoints, static diagnostics, and defensive validation. The emphasis is on choosing an appropriate response: some failures should be corrected during development, some should be reported to users, and others should be recovered from safely.

Learning Objectives

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

  • distinguish syntax, runtime, logical, and semantic errors;
  • explain how exceptions transfer control in Python and C++;
  • use assertions for internal invariants and precondition checks;
  • apply defensive validation at program boundaries;
  • use logging, tracing, breakpoints, and static analysis to diagnose faults;
  • design exception handling that preserves useful information without hiding defects;
  • analyze a failure scenario and select an appropriate recovery strategy;
  • evaluate the limitations, ethical implications, and operational risks of debugging practices.

1. A Taxonomy of Software Errors

An error is a condition that causes a program or development process to deviate from its intended behavior. The following categories overlap in practice but are useful for diagnosis.

1.1 Syntax errors

A syntax error violates the grammar of a programming language. The compiler or interpreter can identify the problem before the affected program is executed.

Python example:

```python

if score > 50

print("Pass")

```

The missing colon causes a `SyntaxError`. In C++, a missing semicolon or unmatched brace commonly produces a compiler diagnostic. Syntax errors are usually local, but the line reported by a compiler is not always the true source: an unmatched quotation mark or brace earlier in a file may cause later lines to be reported incorrectly.

Syntax diagnostics should be read as evidence rather than unquestionable explanations. A useful process is to inspect the reported line and the surrounding statements, then check recently edited delimiters, indentation, names, and punctuation.

1.2 Runtime errors

A runtime error occurs after execution begins. Typical examples include division by zero, opening a missing file, accessing an invalid index, dereferencing an invalid pointer, and exhausting available memory.

Python:

```python

number = int("not a number") # ValueError

```

C++:

```cpp

std::vector<int> values{10, 20};

std::cout << values.at(5); // std::out_of_range

```

Runtime errors may be represented by exceptions, error codes, invalid return values, or undefined behavior. These mechanisms are not equivalent. An exception is a language-supported transfer of control; undefined behavior means that the language specification imposes no reliable requirements on the result. In C++, accessing an array outside its valid bounds can produce undefined behavior if an unchecked operation is used. A robust design prevents such access rather than attempting to interpret its consequences.

1.3 Logical errors

A logical error occurs when a program runs but computes or performs the wrong result. For example:

```python

average = total / count + 1

```

If the intended computation is `total / (count + 1)`, the program may execute normally while producing incorrect values. Logical errors are often more difficult than syntax errors because the runtime supplies no automatic indication that the result is wrong.

Tests, invariants, boundary analysis, code review, and comparison with an independently derived result are important tools for finding logical errors.

1.4 Semantic errors

A semantic error occurs when code has valid syntax and perhaps valid execution, but its meaning does not match the requirement or domain concept. Consider:

```python

is_eligible = age >= 18 and has_id

```

This may be syntactically and logically consistent, yet semantically wrong if the actual policy allows eligibility at age 21 or accepts a digital identity instead. Semantic errors arise from ambiguous requirements, incorrect domain assumptions, misleading names, and inappropriate abstractions.

The distinction between logical and semantic errors is useful but not absolute. A faulty policy implementation is a logical defect in code and a semantic defect relative to the business domain. Resolving such errors requires communication with stakeholders, not merely debugging syntax.

1.5 Error, fault, and failure

A useful engineering vocabulary separates three concepts:

  • A fault is a defect in a program, design, configuration, or requirement.
  • An error state is an incorrect internal state produced by a fault.
  • A failure is observable behavior that does not meet a required service or output.

For example, an incorrect conversion factor is a fault. It produces an erroneous calculated quantity, which becomes a failure when the application displays an incorrect invoice. This distinction supports root-cause analysis: fixing only the visible failure may leave the underlying fault in place.

2. Exceptions as Structured Failure Handling

An exception represents an unusual condition that interrupts normal control flow and transfers control to a handler. It separates the code detecting a problem from the code deciding how to respond.

2.1 Python exception handling

Python uses `try`, `except`, `else`, and `finally` blocks.

```python

try:

quantity = int(input("Quantity: "))

except ValueError:

print("Quantity must be a whole number.")

else:

print(f"Accepted: {quantity}")

finally:

print("Input attempt completed.")

```

The `try` block contains code that may raise an exception. A matching `except` block handles it. The `else` block runs only if no exception occurs in the `try` block. The `finally` block runs whether an exception occurs or not, making it suitable for cleanup such as closing resources. In modern Python, a context manager is often preferable:

```python

with open("records.txt", encoding="utf-8") as file:

data = file.read()

```

The `with` statement ensures that the file is closed even if an exception occurs.

Catch specific exceptions rather than using a broad handler:

```python

try:

price = float(text)

except ValueError:

print("Enter a numeric price.")

```

`except Exception:` can be appropriate at a carefully chosen application boundary for logging and controlled shutdown, but it should not be used to conceal programming defects. Catching `BaseException` is generally inappropriate because it also captures signals such as interruption and system exit.

2.2 Raising exceptions

A function should raise an exception when it cannot fulfill its contract and the caller can reasonably decide what to do.

```python

def withdraw(balance, amount):

if amount < 0:

raise ValueError("amount cannot be negative")

if amount > balance:

raise ValueError("insufficient balance")

return balance - amount

```

An exception message should explain the violated condition without exposing confidential information. Custom exceptions can express domain meaning:

```python

class InsufficientFundsError(Exception):

pass

```

Use exception chaining when a lower-level failure is translated into a higher-level one:

```python

try:

raw = load_account_file()

except OSError as error:

raise AccountStorageError("Account data could not be loaded") from error

```

The original cause remains available for diagnosis while the public interface communicates a domain-level problem.

2.3 C++ exceptions

C++ uses `throw`, `try`, and `catch`.

```cpp

#include <stdexcept>

int divide(int numerator, int denominator) {

if (denominator == 0) {

throw std::invalid_argument("denominator must not be zero");

}

return numerator / denominator;

}

try {

int result = divide(10, 0);

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

std::cerr << error.what();

}

```

Catch exceptions by `const` reference when possible. This avoids unnecessary copying and preserves polymorphic behavior. Handlers should generally be ordered from specific to general. A `catch (...)` handler can serve as a final boundary for logging or safe termination, but it cannot recover intelligently from every possible exception.

C++ resource management should rely on RAII—resource acquisition is initialization. Objects such as `std::ifstream`, `std::lock_guard`, and smart pointers release resources automatically when they leave scope, including during exception unwinding. This is safer than manually pairing allocation and cleanup statements.

2.4 When not to use exceptions

Exceptions are unsuitable for every condition. Expected input rejection, such as an empty form field, may be better represented by validation results. In performance-sensitive inner loops, exceptions should not be used as ordinary branching. Conversely, silently returning a special value can be dangerous when callers may ignore it. The choice depends on the API contract, frequency of the condition, recoverability, and language conventions.

3. Assertions and Invariants

An assertion is an executable statement of an assumption that should be true at a particular point. Python uses `assert`; C++ commonly uses `assert` from `<cassert>`.

```python

def percentage(part, whole):

assert whole > 0, "whole must be positive"

return 100 * part / whole

```

Assertions are valuable for internal invariants, programmer mistakes, and assumptions that indicate a defect if violated. They are not a substitute for user validation. Python can disable assertions with optimization options, and C++ assertions may be disabled by defining `NDEBUG`. Therefore, security checks, permission checks, and validation of untrusted input must remain active ordinary logic.

An invariant is a property expected to remain true during a computation. For a sorted list, an invariant might be that every element before position `i` is no greater than the next element. Stating invariants makes algorithms easier to reason about and helps locate the point at which a defect first appears.

4. Defensive Programming and Validation

Defensive programming anticipates invalid, incomplete, inconsistent, or hostile conditions and makes them explicit. It does not mean adding arbitrary checks everywhere. Good defensive design places checks at trust boundaries and preserves clear contracts.

4.1 Trust boundaries

A trust boundary is a point at which data enters a component from a less controlled source. Examples include user interfaces, HTTP requests, files, environment variables, databases, third-party services, and inter-process messages. Validate data as close as possible to the boundary, then represent validated data in a form that reduces repeated checking.

Validation should address:

  • Type: Is the value an integer, string, date, or structured object?
  • Range: Is it within permitted minimum and maximum limits?
  • Format: Does it match the required pattern?
  • Relationship: Are multiple fields mutually consistent?
  • Authorization: Is the caller allowed to perform the operation?
  • Size: Is the input small enough to process safely?

Validation is not the same as sanitization. Validation rejects values that do not meet a contract; sanitization transforms values. Security-sensitive systems should avoid relying on transformation alone.

4.2 Example of layered validation

```python

def parse_quantity(text):

if not isinstance(text, str):

raise TypeError("quantity input must be text")

cleaned = text.strip()

if not cleaned:

raise ValueError("quantity is required")

try:

quantity = int(cleaned)

except ValueError as error:

raise ValueError("quantity must be a whole number") from error

if not 1 <= quantity <= 1000:

raise ValueError("quantity must be between 1 and 1000")

return quantity

```

The function checks type, presence, convertibility, and range. It returns a valid integer, allowing later code to operate under a stronger assumption.

4.3 Fail-fast and graceful degradation

Fail-fast behavior stops at the earliest point where continuing would make the state unreliable. It is appropriate for violated internal invariants and corrupted configuration. Graceful degradation is appropriate when a nonessential feature fails but the core service can continue, such as displaying cached information when a recommendation service is unavailable.

A recoverable system should define what happens after failure: retry with limits, use a fallback, ask the user to correct input, roll back a transaction, queue work for later, or terminate safely. Retrying is not automatically safe; repeated operations can duplicate payments or intensify an outage. Idempotency, timeouts, backoff, and retry limits are essential in distributed systems.

5. A Systematic Debugging Process

Debugging is disciplined investigation rather than random editing.

5.1 Reproduce and characterize

First obtain a repeatable failure. Record the input, environment, version, time, relevant configuration, and expected versus actual behavior. Reduce the problem to a minimal reproducible example. A failure that cannot be reproduced may still be real, but diagnosis becomes probabilistic.

5.2 Form and test hypotheses

Identify the earliest incorrect observation, not merely the final crash. Form a hypothesis such as “the value becomes negative after the conversion step.” Add a targeted observation or test that could disprove it. Change one relevant factor at a time and preserve a record of findings.

5.3 Use diagnostic tools

Static diagnostics analyze code without executing the complete application. Compilers, linters, type checkers, formatters, and security analyzers can detect unreachable code, suspicious comparisons, missing returns, type mismatches, unsafe operations, and style patterns associated with defects. Static tools produce false positives and false negatives; their results require judgment.

Logging records events for later analysis. A useful log entry includes timestamp, severity, component, correlation or request identifier, and relevant non-sensitive context. Common levels include debug, information, warning, error, and critical. Logging should be structured where possible so that systems can search and aggregate fields.

Never place passwords, authentication tokens, full payment-card numbers, or unnecessary personal data in logs. Logs require access control, retention policies, integrity protection, and sometimes regulatory review.

Tracing follows execution through functions or distributed services. A stack trace identifies the call path associated with an exception. Distributed tracing adds a trace identifier and spans so that a request can be followed across services. Traces should be sampled and protected because they may contain sensitive data and impose storage costs.

Breakpoints pause execution at selected lines. Conditional breakpoints stop only when an expression is true; watchpoints pause when a variable changes. Inspecting the call stack, local state, and thread state often reveals where an invariant first fails.

Tests provide executable evidence. Unit tests isolate functions, integration tests examine component interactions, and end-to-end tests evaluate complete workflows. Boundary-value tests, property-based tests, mutation testing, and regression tests extend coverage beyond typical examples.

6. A Worked Example: Diagnosing a Faulty Grade Importer

Consider the following illustrative Python function:

```python

def average_grade(values):

total = 0

for value in values:

total += int(value)

return total / len(values)

```

Problem

A university import service sometimes crashes and sometimes accepts invalid grades. An input file contains the values `['80', ' 90 ', 'absent']`; another file can be empty.

Analysis

The string `'absent'` produces a `ValueError`, while an empty list produces `ZeroDivisionError`. Even if both exceptions are caught at a high-level interface, the function has no clear policy for absent students and no explicit range validation. A grade such as `140` is syntactically numeric but semantically invalid.

Approach

The contract must be clarified. Suppose the policy is: numeric grades must be from 0 through 100; the token `absent` is represented as missing and excluded from the average; an empty set of numeric grades is an error.

```python

class GradeDataError(ValueError):

pass

def average_grade(values):

grades = []

for position, raw in enumerate(values, start=1):

if not isinstance(raw, str):

raise GradeDataError(f"record {position}: expected text")

token = raw.strip().lower()

if token == "absent":

continue

try:

grade = int(token)

except ValueError as error:

raise GradeDataError(

f"record {position}: invalid grade"

) from error

if not 0 <= grade <= 100:

raise GradeDataError(f"record {position}: grade out of range")

grades.append(grade)

if not grades:

raise GradeDataError("no numeric grades available")

return sum(grades) / len(grades)

```

For `['80', ' 90 ', 'absent']`, the numeric grades are 80 and 90, so the result is `(80 + 90) / 2 = 85.0`. The function now rejects malformed and out-of-range records with a domain-specific exception. The boundary layer can catch `GradeDataError`, report the file and record number, and place the source file in a review queue rather than crashing the whole import service.

Outcome and learning points

The original problem combined runtime failures with semantic ambiguity. Debugging alone could locate the exception, but requirement clarification was necessary to define correct behavior. The improved design validates at the boundary, preserves context, distinguishes expected data problems from programmer defects, and makes the empty-input policy explicit.

7. Exception-Handling Design Principles

A robust exception strategy follows several principles:

1. Handle only what you can resolve. If a layer cannot recover, allow the exception to propagate to a suitable boundary.

2. Preserve context. Include operation and resource information, but avoid secrets and excessive personal data.

3. Do not silently continue. Suppressing an exception can convert a visible failure into corrupted output.

4. Maintain atomicity. A multi-step update should either complete consistently or roll back.

5. Separate user messages from diagnostic details. Users need actionable explanations; developers need technical context.

6. Use stable error contracts. APIs should document error types, status codes, or result structures so callers can respond predictably.

7. Test failure paths. Recovery code is production code and requires tests, monitoring, and maintenance.

In web applications, internal exceptions should not be returned as raw stack traces to users. A service can log the detailed trace with a correlation identifier and return a generic message with an appropriate status. This reduces information leakage while supporting investigation.

8. Limitations, Ethics, and Governance

Debugging tools can expose source code, customer data, credentials, and system topology. Developers must follow least-privilege access, masking rules, retention limits, and organizational policies. Production debugging should be authorized, auditable, and preferably performed using sanitized replicas or sampled data.

AI-assisted debugging can suggest likely causes, generate tests, summarize stack traces, and identify patterns in logs. However, generated suggestions may be incorrect, may reproduce insecure code, or may disclose confidential source and data to an external service. Developers remain responsible for verifying fixes, reviewing licensing and privacy implications, and testing behavior under adversarial inputs. AI output is a hypothesis, not evidence.

A technically successful fix may still be unacceptable if it discriminates against users, conceals an incident, violates data-protection obligations, or removes an audit trail. Error handling is therefore part of software governance as well as programming technique.

Best-Practice Checklist

  • Compile or run static checks early and often.
  • Read the entire diagnostic context, not only the first highlighted line.
  • Reproduce failures with controlled, minimal inputs.
  • Validate all external input at trust boundaries.
  • Use assertions for internal assumptions, not security controls.
  • Catch specific exceptions and preserve their causes.
  • Use RAII or context managers for resource cleanup.
  • Log structured, useful, and non-sensitive information.
  • Test boundary, failure, timeout, retry, and recovery paths.
  • Define whether a failure should stop, retry, roll back, fall back, or be reported.
  • Review fixes for security, privacy, accessibility, and maintainability.

Chapter Summary

Errors may be syntactic, runtime, logical, or semantic, and each category calls for different evidence and corrective action. Exceptions provide structured control flow for abnormal conditions, while assertions document internal invariants and should not replace validation of untrusted input. Defensive programming validates data at trust boundaries, establishes explicit contracts, and chooses an appropriate response such as rejection, recovery, rollback, retry, or safe termination.

Effective debugging combines reproducible cases, hypothesis testing, static diagnostics, logging, tracing, breakpoints, and automated tests. Professional exception handling avoids both silent suppression and indiscriminate catching. It preserves context, protects sensitive information, and maintains consistent system state. In contemporary development, observability tools and AI assistance can accelerate diagnosis, but privacy, security, governance, and human verification remain essential.

Key Terms

Syntax error: A violation of a programming language’s grammar.

Runtime error: A failure that occurs while a program is executing.

Logical error: A defect that causes an incorrect computation or decision while execution continues.

Semantic error: A mismatch between the meaning of a program and the intended requirement or domain concept.

Fault: A defect in code, design, configuration, or requirements.

Failure: Observable behavior that does not meet a required expectation.

Exception: A structured signal that transfers control from normal execution to a handler.

Exception handler: Code that responds to a raised exception.

Assertion: An executable statement that an assumed condition must be true.

Invariant: A property that should remain true at a defined point or throughout an operation.

Defensive programming: Designing software to anticipate invalid or unexpected conditions.

Trust boundary: A point where data enters from a less controlled source.

Static analysis: Examination of code without executing the complete program.

Logging: Recording runtime events and contextual information for diagnosis or auditing.

Tracing: Following execution across calls or services.

Breakpoint: A debugging control that pauses execution at a selected location.

RAII: A C++ resource-management principle that ties resource lifetime to object lifetime.

Fail-fast: Stopping promptly when continuing would make the state unreliable.

Graceful degradation: Continuing reduced service when a nonessential component fails.

Idempotency: A property in which repeating an operation has the same intended effect as performing it once.

Review Questions

Short Answer Questions

1. What is the difference between a logical error and a runtime error?

2. Why should exceptions generally be caught specifically rather than broadly?

3. What is the purpose of an assertion?

4. Name two types of information that should not normally be written to logs.

5. What is a trust boundary?

Long Answer Questions

1. Explain the roles of `try`, `except`, `else`, and `finally` in Python exception handling.

2. Compare fail-fast behavior with graceful degradation and give a suitable use case for each.

3. Explain how RAII in C++ supports safe resource management during exception unwinding.

4. Describe a systematic debugging process from reproduction through verification of a fix.

5. Discuss why semantic errors often require communication with domain stakeholders.

Analytical / Application Questions

1. Design a validation and exception strategy for a web endpoint that receives a customer’s age, account identifier, and payment amount. Identify trust-boundary checks and suitable responses.

2. A service catches every exception, writes “operation failed” to a log, and returns an empty result. Analyze the risks and propose a safer design.

3. A batch import fails after updating half of a database table. Explain how you would investigate the defect and redesign the operation to preserve consistency.

Multiple Choice Questions with Answers

Question 1

Which type of error occurs when a program runs but produces an incorrect result because an expression implements the wrong computation?

A. Syntax error

B. Logical error

C. Linker error

D. Resource error

Correct Answer: B. Logical error

Question 2

Which Python block runs only when no exception is raised in the try block?

A. except

B. finally

C. else

D. raise

Correct Answer: C. else

Question 3

What is the most appropriate primary use of an assertion?

A. Validating passwords supplied by users

B. Handling normal form input rejection

C. Documenting and checking an internal invariant

D. Replacing all exception handling

Correct Answer: C. Documenting and checking an internal invariant

Question 4

Why should a Python exception usually be caught as a specific exception type?

A. To make every error recoverable

B. To avoid handling unrelated defects unintentionally

C. To disable stack traces

D. To improve arithmetic precision

Correct Answer: B. To avoid handling unrelated defects unintentionally

Question 5

Which practice is characteristic of defensive programming?

A. Trusting all data from external systems

B. Removing validation to improve performance

C. Validating input at a trust boundary

D. Suppressing every exception

Correct Answer: C. Validating input at a trust boundary

Question 6

What does a debugger breakpoint normally do?

A. Deletes the current function

B. Pauses execution at a selected location

C. Converts runtime errors into syntax errors

D. Automatically proves program correctness

Correct Answer: B. Pauses execution at a selected location

Question 7

Which C++ principle automatically releases many resources when an object leaves scope?

A. RAII

B. FIFO

C. MVC

D. DNS

Correct Answer: A. RAII

Question 8

For the grades 80 and 90, excluding an 'absent' record according to the chapter’s example, what is the average?

A. 80.0

B. 85.0

C. 90.0

D. 170.0

Correct Answer: B. 85.0

Question 9

Which item should generally not be placed in an application log?

A. A correlation identifier

B. An event severity

C. A password or authentication token

D. The affected component

Correct Answer: C. A password or authentication token

Question 10

What is a key risk of catching every exception and returning an empty result?

A. It guarantees stronger type checking

B. It may hide defects and make corrupted or missing results appear valid

C. It prevents all resource leaks

D. It ensures that retries are idempotent

Correct Answer: B. It may hide defects and make corrupted or missing results appear valid

Programming Fundamentals (Python/C++)
Chapter 9 of 15