Handling Exceptions
Now that you’ve seen how to generate exceptions, let’s move on to handling them.
Using the try-catch Construct
Exceptions are caught and processed using the try and catch construct, which has the following form:
Code that you suspect might fail is enclosed in a try block that is followed by one or more handlers in the form of catch blocks. Each catch block looks a little like a function definition, with catch followed by a type in parentheses, which represents the type that will be caught and processed by the catch block. In the preceding code, the first catch block will handle exceptions tagged with a TypeOne^ type, while the second block will handle those tagged with a TypeTwo^ type. Try and catch blocks form a single construct. You can’t have a try block without at least one catch block, you can’t have a catch block without a try block, and you can’t put anything in between them. You can chain as many catch blocks together as there are exception types to catch, as long as you have at least one. The following exercise will show you the basics of handling exceptions, using the example from the previous exercise as a basis. |
1. Reopen the project from the previous example if you’ve closed it. Modify the main() function so that it looks like this:
Console::WriteLine(L"Throw Test");
try
{
int n = 3;
Console::WriteLine(L"Calling with n = 3");
func(n);
Console::WriteLine(L"Calling with n = 0");
n = 0;
func(n);
}
catch(System::ArgumentException^ pex)
{
Console::WriteLine(L"Exception was {0}", pex);
}
Console::WriteLine(L"All done");
The calls to the function are enclosed in a try block, which is followed by a single catch block. When the second call to the function fails, the exception-handling mechanism takes over. It can’t find a handler in the function where the error originated, so it walks one level up the call stack and comes out in the try block. At this point, the runtime wants to go off looking for a handler. As part of this process, it puts the program stack back to where it was at the start of the try block. In other words, it unwinds the stack, which means that it destroys any variables that have been created on the stack within the try block, so you can’t use them in the catch block. You need to bear this in mind when writing exception handlers and declare any variables you need to use in the catch block outside the corresponding try. When the stack has been unwound, the code looks at the catch blocks associated with this try block to see whether there is one that has an argument type that matches what was thrown. In this case, we have a match, so the contents of the catch block are executed. If there wasn’t a suitable catch block, the runtime would try to move up another level of the call stack and then would fail and terminate the program.
2. Execute this code. You should see something very similar to the following figure. Take note that the JIT Debugger not invoked anymore.
The second function call has generated an exception that has been caught by the catch block, which has printed out “Exception was:” plus the exception details. In contrast to what happened in the previous exercise, the final “All done” message is now printed. This illustrates an important point about exception handling: once a catch block has been executed, program execution continues after the catch block as if nothing had happened. If there are any other catch blocks chained to the one that is executed, they’re ignored.
3. Try changing the second call so that it passes in a positive value. You’ll find that the catch block isn’t executed at all as shown below when the second call, n = 4.
If a try block finishes without any exception occurring, execution skips all the catch blocks associated with the try block.
Customizing Exception Handling
Just printing out the exception object results in the type-plus-message-plus-stack trace that you saw when the exception was unhandled. You can use properties of the Exception class to control what is printed, as shown in the following table.
System::Exception Property | Description |
Message | Returns a string containing the message associated with this exception. |
StackTrace | Returns a string containing the stack trace details. |
Source | Returns a string containing the name of the object or application that caused the error. By default, this is the name of the assembly. |
Table 2 |
If you altered the WriteLine statement in the catch block to read like this:
catch(System::ArgumentException^ pex)
{
Console::WriteLine(L"Exception was {0}", pex->Message);
}
you’d expect to see a result like this:
Exception was Aaargh! What wrong?
In a similar way, you could use StackTrace to retrieve and print the stack trace information.
Using the Exception Hierarchy
The exception classes form a hierarchy based on System::Exception, and you can use this hierarchy to simplify your exception handling. As an example, consider System::ArithmeticException, which inherits from System::Exception and has subclasses that include System::DivideByZeroException and System::OverflowException. Now look at the following code:
try
{
// do some arithmetic operation
}
catch(System::ArithmeticException^ pex)
{
// handle this exception
}
catch(System::DivideByZeroException^ pex)
{
// handle this exception
}
Suppose a DivideByZeroException is thrown. You might expect it to be caught by the second catch block, but it will, in fact, get caught by the first one. This is because, according to the inheritance hierarchy, a DivideByZeroException is an ArithmeticException, so the type of the first catch block matches. To get the behavior you expect when using more than one catch block, you need to rank the catch blocks from most specific to most general. The compiler will give you warning C4286 if you get the catch blocks in the wrong order. This works for both managed and unmanaged code. So, if you just want to catch all arithmetic exceptions, you can simply put in a handler for ArithmeticException, and all exceptions from derived classes will get caught. In the most general case, you can simply add a handler for Exception, and all managed exceptions will be caught.
Using Exceptions with Constructors
One of the advantages of exceptions mentioned before is: they enable you to signal an error where there’s no way to return a value. They’re very useful for reporting errors in constructors, which, as you now know, don’t have a return value. In the following exercise, you’ll see how to define a simple class that uses an exception to report errors from its constructor, and you’ll also see how to check for exceptions when creating objects of this type.
1. Start Visual C++/Studio .NET, and create a new CLR Console Application project named CtorTest.
2. Immediately after the using namespace System; line and immediately before main(), add the following class definition:
ref class Test
{
String^ pv;
public:
Test(String^ pval)
{
// test for null pointer or empty string
if (pval == nullptr || pval == L"")
throw gcnew System::ArgumentException(L"Argument null or blank");
else
pval = pv;
}
};
The ref keyword makes this class managed, and this managed class has one simple data member, a pointer to a managed String. At construction time, this pointer must not be null or point to a blank string, so the constructor checks the pointer and throws an exception if the test fails. If the pointer passes the test, construction continues.
3. Try creating an object in the main() function, like this:
int main(array<System::String ^> ^args)
{
Console::WriteLine(L"Exceptions in Constructors");
// Create a null pointer to test the exception handling
String^ ps = nullptr;
Test^ pt = nullptr;
// Try creating an object
try
{
pt = gcnew Test(ps);
}
catch(System::ArgumentException^ pex)
{
Console::WriteLine(L"Exception: {0}", pex->Message);
}
Console::WriteLine(L"Object construction finished");
return 0;
}
![]() |
Notice that the call to gcnew is enclosed in a try block. If something is wrong with the String pointer (as it is here), the Test constructor will throw an exception that will be caught by the catch block.
4. Build and run your program and the following output should be expected.
5. Try modifying the declaration of the ps string so that it points to a blank string (initialize it with L"") as shown below.
6. And then try a non-blank string, to check that the exception is thrown correctly as shown below.