Object-Oriented Programming

Errors and Exception Handling

Michael L. Collard, Ph.D.

Department of Computer Science, The University of Akron

Importance of Error Handling

  • Often ignored and more complex than anticipated
  • Design for the "happy path": method naming, parameter types, etc.
  • Design of error handling for non-happy path introduces further complexity to an already existing API
  • Includes what, e.g., int values, strings, and particular values

Error Handling Choices

  • Error Return
  • Error Parameter
  • Global State

Error Return

  • Return error code
  • Typically, an int or bool
  • Check value after each use (which is easy to forget)
  • Return error may be mixed into normal processing return

Error Parameter

  • A parameter contains an error code
  • Similar to error return except not mixed with normal processing return
  • Have to declare a variable before the call

Global Error Value

  • A global variable contains the error results
  • May be mixed with error return or error parameter for more information
  • For perror(), the global errno contains the last error code
  • Code to reset or get the global, shared value may be confusing

Problems with Error Codes

  • Do not work with constructors, as constructors do not return a value
  • Error codes do not have consistent values across applications
  • libarchive
  • libcurl
  • Numeric results are not obvious, and name forms are often used/needed
  • Propagating an error code is tedious

Error Propagation Flow

  • Calls to h() only get h() errors
  • Calls from h() to g() and g() to f() are not propagated
  • Every call to a function with an error return or parameter must pass on that error to the calling function
  • Disturbs normal processing
  • Even with one layer missing, it breaks the error propagation
  • Not easy to do correctly

Exception Handling by Keyword

  • throw Where an error is detected, the function/method immediately returns from that point in the code
  • catch Layered calls are returned (stack unwinding) until one that matches is found
  • try The catch is associated with the code in the try block

Semantics

  • resumption semantics When the code generates an exception, go to where caught, process catch, then return to the original point in the code
  • termination semantics When the code generates an exception, go to where caught, process catch, and continue in the code at the catch point
  • C++ only supports termination semantics

How to use Exceptions

exception-handling code is error-handling code

  • Separates the "good path" (or "happy path") from the "bad path"
  • Errors where the code cannot handle the error locally, e.g., a constructor
  • Do not use exception handling for normal processing
  • Since they automatically deallocate, RAII is a key technique to use for all variables

Why RAII is Important

Partial std::exception Hierarchy

std::logic_error

faulty logic within the program, such as violating logical preconditions or class invariants, and may be preventable

  • std::invalid_argument argument value is not accepted
  • std::domain_error inputs are outside of the domain defined for the operation
  • std::length_error exceed implementation-defined length limits for some object
  • std::out_of_range access elements out of defined range

std::runtime_error

errors due to events beyond the scope of the program and where the program code cannot easily predict the error

  • std::range_error the destination type cannot represent the result of a computation
  • std::overflow_error arithmetic overflow errors, i.e., the result of a computation is too large for the destination type
  • std::underflow_errorthe result of a computation is a subnormal floating-point value

catch Handlers

  • Parameter modifiers const and volatile are ignored
  • catch (...) matches any type
  • catch (Foo& e) matches any object of type Foo or any object of any class derived from Foo
  • Order matters: Put derived class catch handlers before base class catch handlers

Goal: One try per Function

function try block

Recommendations

  • Throw objects instead of scalar values
  • It is possible to throw any type, including int and char*, but more challenging to determine what the error is
  • Throw objects derived from std::exception
  • That way, catch handlers for std::exception will catch your throw
  • Catch by reference, i.e., &
  • const is thrown away; not concerned about the advantages of const at this point
  • Can also catch by pointer, but pointers cause the question: "Who should free?"

Constructors

throw;

Exception Dispatcher: Re-Throw Idiom

noexcept specifier

  • Current:
  • non-throwing - void f() noexcept;
  • potentially throwing - void f();
  • Deprecated:
  • non-throwing - void f() throw();
  • potentially throwing - void f() throw(std::exception);

Other Issues

  • C++ has no "finally"; use RAII instead
  • Exception handling may or may not be slow but shouldn't cause a slowdown of normal processing
  • If a framework throws pointers, use pointers (e.g., MFC)

Overall Design

  • Inherit from the standard exceptions, e.g., std::exception and family
  • In general, one try block per function. Good idea to extract function if more than one is needed
  • Organize around the logical reason for the exception, not the source (e.g., subsystem)
  • E.g., Banking app, Reason: "insufficient funds", Subsystem: One of many
  • Design for the entire system
  • Avoid a layer-by-layer design

Scenario: Starting Code

Scenario: Adding Exception Handling

Problem

Scenario: Summation

  • In C++, the destructor cannot be called for a partially-constructed object
  • When a constructor throws an exception, the destructor of the class cannot clean up fields/data members
  • Every data member should have RAII semantics and not rely on class destructor