Error handling is an essential part of robust and reliable software development. This lab will introduce you to the concepts and mechanisms of error handling in C++. While this lab is focused on C++-specific features, many concepts demonstrated here are also applicable in other languages.
§1 Types of Errors #
Syntax Errors:
- Occur when the code violates the syntax rules of the C++ language
- Detected by the compiler during compilation
int x = ; // Missing initializer
Logical Errors:
- The program compiles and runs but produces incorrect results.
- Caused by flaws in the program logic.
int divide(int a, int b) { return a + b; } // should divide instead of add
Runtime Errors:
- Occur during the execution of the program.
- Examples: dividing by zero, accessing invalid memory, file not found.
int arr[3] = {1, 2, 3};
std::cout << arr[3]; // Out-of-bounds access
§2 Error Handling Schemes #
C++ provides several complementary strategies for detecting, reporting, and recovering from runtime problems. This section introduces three of the most common schemes: return codes, error flags, and exceptions. Each approach has a trade-offs between code clarity, performance, and maintainability.
Return Codes:
Use return values to indicate success or failure. 0 typically signifies success, all other values signify failure. The meaning of the return codes are arbitrarily defined by the programmer.
int divide(int numerator, int denominator, int& result) {
if (denominator == 0) return -1; // Error: Division by zero. return 1
result = numerator / denominator;
return 0; // Success. return 0
}
int main() {
int res;
if (divide(10, 0, res) == -1) {
std::cerr << "Error: Division by zero" << std::endl;
} else {
std::cout << "Result: " << res << std::endl;
}
}
Note: the
main()function utilizes a return code, returning any value other than0is interpreted as the program exiting with a failure.
Error Flags:
Use status flags within objects (e.g., std::ios::fail() in file streams).
int main() {
std::ifstream file("nonexistent.txt");
if (file.fail()) { // use error flag to check for issues
std::cerr << "Error: Could not open the file." << std::endl;
} else {
std::cout << "File opened successfully." << std::endl;
}
}
Exceptions:
A structured mechanism to handle errors by separating error-handling logic from normal code flow.
int safeDivide(int numerator, int denominator) {
if (denominator == 0) throw std::invalid_argument("ERROR: Division by zero");
return numerator / denominator;
}
int main() {
try {
int result = safeDivide(10, 0);
std::cout << "Result: " << result << std::endl;
} catch (const std::exception& ex) {
std::cerr << "Exception: " << ex.what() << std::endl;
}
}
./exception-example
ERROR: Division by zero
§3 Asynchronous vs. Synchronous Errors #
Errors in programs fall into two main categories: synchronous and asynchronous. Synchronous errors occur during normal code execution, such as accessing an invalid array index or a missing file. Synchronous errors be caught with standard error-handling. Asynchronous errors arise unexpectedly from external events such as hardware failures or system interrupts, these types of errors are harder to predict and manage.
Synchronous Errors:
- Occur during the execution of a specific code block.
- Examples: Invalid file access, out-of-bound array access.
Asynchronous Errors:
- Occur independently of the program’s control flow.
- Examples: Hardware failures, external signal interruptions.
§4 Exception Handling: try, throw, and catch #
Exception handling in C++ enables programs to handle runtime errors gracefully. This is achieved using try, throw, and catch. The try block contains code that has potential to fail, throw signals an error has occurred, and catch handles the error. This combination can be used to prevent crashes and improving reliability.
Basic Syntax:
tryBlock: Encapsulates code that may throw exceptions.throwStatement: Used to signal an error condition by throwing an exception.catchBlock: Handles the exception. Example:
void divide(int a, int b) {
if (b == 0)
throw std::runtime_error("Division by zero!"); // throw error
std::cout << "Result: " << a / b << std::endl;
}
int main() {
try { // "try" code that may throw an error
divide(10, 0);
} catch (const std::exception& e) { // "catch" error and handle it gracefully
std::cerr << "Error: " << e.what() << std::endl;
}
}
./exception-example
Error: Division by zero!
§5 Stack Unwinding #
When an exception is thrown, C++ cleans up the stack by calling destructors for all objects created in the try block before the exception was thrown.
Steps:
- Exception is Thrown:
- When an exception is thrown, the normal flow of execution is interrupted, and the program searches for an appropriate exception handler (
catchblock).
- When an exception is thrown, the normal flow of execution is interrupted, and the program searches for an appropriate exception handler (
- Function Exits:
- Functions in the call stack begin to exit in reverse order (from the most recent call downwards). This process is known as unwinding the stack.
- As functions exit, their local objects are destroyed, and their destructors (if any) are called.
- Catch Block Found:
- The stack unwinding stops as soon as a matching
catchblock is found. - If no suitable handler is found, the program terminates.
- The stack unwinding stops as soon as a matching
Example:
class Resource {
public:
Resource(const std::string& name) : name(name) {
std::cout << "Acquiring " << name << std::endl;
}
~Resource() {
std::cout << "Releasing " << name << std::endl;
}
private:
std::string name;
};
void faultyFunction() {
Resource res1("Resource1"); // creates object, this gets unwound last
Resource res2("Resource2"); // creates object, this gets unwound first
throw std::runtime_error("Something went wrong");
Resource res3("Resource3"); // Never executed
}
int main() {
try {
faultyFunction(); // call faulty function
} catch (const std::exception& e) {
std::cerr << "Caught: " << e.what() << std::endl; // catch thrown error
}
}
$ ./unwinding-example
Acquiring Resource1
Acquiring Resource2
Releasing Resource2
Releasing Resource1
Caught: Something went wrong
§6 Re-throwing an Exception #
In C++, re-throwing an exception allows you to propagate an already caught exception to a higher-level catch block. This can be useful when a lower-level function or code block handles part of the exception’s consequences but the exception requires further processing by a higher-level handler.
To re-throw an exception, use the throw; statement without specifying an exception object. This continues the propagation of the currently handled exception.
Example:
void level2() {
std::cerr << "Running faulty function." << std::endl;
throw std::runtime_error("Error in level 2");
}
void level1() {
try {
level2();
} catch (...) { // ellipsis to catch any type of error
std::cerr << "Handling at level 1, re-throwing..." << std::endl;
throw; // Re-throws the same exception
}
}
int main() {
try {
level1();
} catch (const std::exception& e) { // catch error object to use in output
std::cerr << "Caught at main: " << e.what() << std::endl;
}
}
./rethrow-example
Running faulty function.
Handling at level 1, re-throwing...
Caught at main: Error in level 2
Explanation:
level2()throws astd::runtime_errorwith the message"Error in level 2".level1()catches any exception with(...), logs a message, and re-throws the exception usingthrow;.main()catches the re-thrown exception aseand logs the error message withe.what()
§7 Further Reading #
- Textbook Chapter 16.1: Exceptions
- https://learn.microsoft.com/en-us/cpp/cpp/errors-and-exception-handling-modern-cpp?view=msvc-170
- https://en.cppreference.com/w/cpp/language/exceptions.html
§8 Questions #
- Why are logical errors more challenging to debug than syntax and runtime errors?
- What happens when an array is accessed out of its bounds in C++? What type of error is this an example of?
- What is the purpose of return codes in error handling?
- How does the following code use return codes to handle errors?
int divide(int numerator, int denominator, int& result) {
if (denominator == 0) return -1;
result = numerator / denominator;
return 0;
}
- What C++ feature allows you to check for issues in file operations like opening a file?
- What happens when an exception is thrown in C++?
- How does the try, throw, and catch mechanism work in C++?
- What is stack unwinding in the context of exception handling?
- In the following code, why does “Resource3” never get acquired?
void faultyFunction() {
Resource res1("Resource1");
Resource res2("Resource2");
throw std::runtime_error("Something went wrong");
Resource res3("Resource3");
}
- What is the benefit of re-throwing an exception?