Error Handling

Exception

An exception is an unscheduled event that disrupts program execution; used to detect undefined instructions.

Example from my Biquadris project (shortened for conciseness)

BlockCommand::BlockCommand(const std::string& name): Command{name} {
    if (name.length() != 1) {
        throw std::invalid_argument{"invalid block type"};
    }

Also see Exception Safety.

CS247

I didn’t really think about exceptions before, but if you want to be a 10x Developer, you need to recognize that exceptions significantly change control flow! We no longer have the guarantee of sequential execution.

You need to think very hard about Exception Safety.

Motivation

To first motivate this, you need to understand Error Handling. C++ enables error handling through the use of Exceptions.

V.at(100) fetches V[100] if the value exists, otherwise, throws an exception.

try {
	cout << v.at(100) << endl;
} catch(std::out_of_range r)  {
	cout << "Range error" << r.what() << endl;
}
  • r is just an object, class type is std::our_of_range (included in <stdexcept>)
  • The .what() method returns a string describing the exception.

Force the programmer to deal with the error because the control flow jumps.

Vector knows the error happened but not how to fix it. We know how to fix it, but not how the error occurred.

To raise an exception ourself, we use the throw keyword. We can throw any value, but keep in mind that <stdexcept> has objects for common scenarios like out_of_range, logic_error, invalid_argument.

Stack Unwinding

When an exception is raised, control flow steps. We search through the stack upwards looking for a handler for this type of exception. This is called Stack Unwinding. Destructors are run for objects stored on the stack during the process of stack unwinding.

If a handler is found, we jump to that point. If no handler is found, program crashes.

Example of stack unwinding:

void f() {
	throw std::out_of_range{"f threw"};
}
void q() { f();};
void h() {q();};
int main() {
	try { h();}
	catch (std::out_of_range r) {
		cout << r.what();
	}
}
  • Main calls h, h calls q, q calls f, throws, stack unwinding through q, h, jump to catch block in main.

Handling Multiple Errors

Multiple errors may be handled via multiple catch blocks.

try {...}
catch (out_of_range r) {...}
catch (logic_error e) {...}
catch (invalid_argument u) {...}
catch(...) {...} // yes, catch(...) is the catch-all syntax which catches any type of exception ... Literally 3 dots

Rethrowing Errors

One handler can also deal with part of an error, rethrow the exception to allow someone else to deal with it (Stack Unwinding occurs).

void calculation(DataStructure& ds) {
	...
	throw (ds_error) {...};
}
 
void DataStructureHandler(DataStructure& ds) {
	try {calculation(ds);}
	catch (ds_error e) {
		// fix the data structure issue
		throw prompt_input_error{...};
	}
}
 
int main() {
	DataStructure ds;
	string s;
	while (cin >> s) {
		try {
			DataStructureHandler(ds);
		} catch (prompt_input_error e) {
			cout << "invalid input";
		}
	}
}

We can also rethrow the same exception for someone else to deal with.

try {...}
catch (std::exception& e) {
	...
	throw; // IMPORTANT, DO NOT DO "throw e";
}

Why here do I just say throw rather than throw e?

This wasn’t just an accident.

  • throw e performs a copy in order to throw this exception
  • Remember that copy constructors (any type of constructor) cannot be virtual. Therefore, the static type is used for the copy

If you throw a range_error and catch via std::exception& catch block,

  • throw rethrows range_error
  • throw e catch a std::exception copied from the range_error, we lose the dynamic type.

Generally, catch blocks should catch by reference to avoid avoid copies.

catch(...) vs catch(std::exception& e)?

The professor introduced both syntaxes, I thought they were exactly the same thing. Turns out, there is a difference:

try {...}
catch(const std::exception& e){} // Will catch `std::exceptions` only.
catch(...) {throw; }; // Will catch everything there after.

With exceptions, you can handle integers and other types (http://www.cplusplus.com/doc/tutorial/exceptions/). I guess, for example, you can throw 5.

For example:

catch(int e)

StackOverflow

Destructor Exceptions

Never let a destructor throw an Exception!

Default behavior: Program immediately crashes.

We can allow exceptions thrown from destructors by tagging them noexcept (false), see noexcept.

For example,

class A {
	public:
		...
		~A() noexcept (false) {...}
}

Be careful with noexcept(false) Destructors

As we know, when we throw an exception, Stack Unwinding occurs. During this process destructors are running for stack allocated objects. If one of these destructors throws, now we have 2 active exceptions!

  • 2 active exceptions = program crash (this behavior cannot be changed)

Standard <stdexcept> errors

Some standard <stdexcept> errors include:

  • out_of_range
  • logic_error
  • invalid_argument
  • std::bad_alloc occurs when the new operator fails to allocate the requested space, more here