What is Exception Handling?
Exceptions in software refer to error conditions that stop the software from executing the regular path. These errors can be something that can be controlled by the software itself such as bad parameters or outside of its control. For example, errors returned by the system calls such as to open a file, a socket, to allocate a block of memory, etc.
Exception Handling offers a better mechanism to detect and handle errors. To understand why it is better, let’s now see how we detect and handle errors without Exception Handling, such as in a Programming language like C.
Traditional Way to Detect and Handle Errors
We usually break our program down into multiple functions or subroutines to make it modular and easier to understand. That means our program will have multiple and chained function calls. Each function may return some information to be used by the caller.
Let’s look at a simple program to add two integers that must be below 100. We may write our program as follows:
All involved functions return an integer and when we encounter an error we return -1. But we are missing something here, -1 may be the result of the addition. We may want to add a check to ensure the arguments are not less than 0. But now, at the main function when we receive an error we don’t know which want it is, < 0 or > 100. We need to add a global variable.
So now we have a mechanism to detect errors and handle them. The function calls may be longer depending on your program.
This is how we handle errors in a traditional way such as when we want to open a file in C Programming Language:
FILE *fp;
fp = fopen ("file.txt", "w+");
if fp == NULL, the operation has failed and a global variable errno is updated.
Some Issues with the Traditional Approach
There are some issues with the traditional approach above. These are some of them:
- All functions involved must return the same type, such as integer to propagate the error status. These function calls may be quite long, and they are forced to return the same type.
- The global variable must be checked immediately after the function call at the handler returns, or cached. Because it may be updated when another error occurs afterward.
- It is up to the caller whether to handle the error or not. If it is not handled, it might cause a crash later in the program or the program continues abnormally.
Handling Errors with Exception Handling
How can exception handling improve the traditional approach?
For starters, it provides a clear separation between the code that knows about the error and the code that knows how to handle the error and everything in between can safely ignore the error.
With Exception Handling, our code now looks different. The function in the middle _add_wrapper() does not have to return integer type. It doesn’t need to know about the error. When an error is thrown, it will be handled in the main()_ function that catches the error. Now no matter how many functions you add in between the error detector and error handler, they all can ignore the error that they should not care about.
And, we can get rid of the global variable.
On the last point, if an exception is not handled, the program will terminate because the C++ runtime will invoke std::terminate.
If we remove try-catch in our code and execute it, this is what we get:
terminate called after throwing an instance of 'std::invalid_argument'
what(): parameters must be >= 0
Aborted (core dumped)
So it is safer, in the sense that the program does not continue running, and we get some information about what happened.
Now that we have seen what Exception Handling offers, let’s dive into more details.
Exception Handling in C++
We have seen above how we can write Exception Handling in C++ which consists of two parts:
- Error detector: where we call throw statement
- Error handler: where we write the try-catch statement
Exceptions work on functions
Exceptions work by adding extra information and code into all the involved functions. Involved functions include not only detector and handler but also all functions in between.
Now you can see that it is not magic, all the involved functions must still have to do something to realize this error reporting.
The difference is that all these works are done by the compiler for us so it is not visible to us from our code. That’s why we need to understand this concept because it is not obvious just by looking at the code.
The extra information and code that is added to all functions that participate in an exception are added to the end of the involved functions’ code, in the area called LSDA (Language Specific Data Area).
The LSDA contains information on whether a function can catch an exception, the type of exception, and how to clean up a function.
As illustrated above, the compiler generates extra code for us when we use exceptions.
The implementation details are language and compiler-specific usually implemented through a function called Personality Function which uses the metadata (see picture above) for the C++ runtime to:
- Search for the handler that can handle the type of exception being thrown
- Execute clean-up routines such as destroying objects
When a throw statement is called, the C++ runtime will search for the handler and execute the clean-up routines in a process called Stack Unwinding. Depending on compilers this may be a one-pass or two-pass process.
In a one-pass process, the search is followed by a clean-up process each time the unwinding goes one function back until try-catch is found.
In the two-pass process, the search is done without cleanup, only if a handler is found the cleanup routines are executed, in the second pass.
Design for Safety
There are some details that we need to know in C++ to ensure that our code is written safely.
The code above may cause a memory leak if add() throws an exception. Because the last two lines are not executed. The compiler can’t generate the cleanup routine for this function, what it can do is to generate cleanup to call destructors, but the temp is not an object.
The easiest way to remember this is not to use pointers in all the involved functions.
Another detail is an exception raised in the constructor. The compiler does not generate a cleanup routine if the exception is thrown in the constructor.
In the code above, we have a memory leak because there is no proper cleanup being done. The compiler does not generate a cleanup routine for ErrorPropagator() function. I believe it is because the object is considered partially created.
To solve this issue, we should avoid using raw pointers in our constructor and use a wrapper instead. And, all our objects should implement RAII (Resource Allocation is Initialization) idiom%20to%20the), to ensure all objects are cleaned up.
This works because the cleanup routine is generated for the constructor function. Let’s look at the correct version below.
This way, we have a cleanup routine generated for our constructor.
Noexcept – breaking the chain
There are some cases where we want to not catch or propagate the exception. C++ provides a way for this by adding a noexcept keyword at the end of the function’s name. If we modify our code above by adding a noexcept keyword to ErrorPropagator() function, the metadata is not generated and the search for error handler will fail which causes the C++ runtime to invoke std::terminate.
void ErrorPropagator() noexcept
{
Error error;
error.Test();
}
The result is:
terminate called after throwing an instance of 'std::invalid_argument'
what(): wrong error parameter
Aborted (core dumped)
Summary and References
We now know what problems can be solved by the Exception Handling, how it works – the compiler adds extra code, and what we need to care about when we write our C++ code.
Understanding this concept will help us writing better C++ code.