3D PLM Enterprise Architecture |
Middleware Abstraction |
Managing Errors Using ExceptionsCreating, analyzing, enriching, and reacting to errors using exceptions |
Technical Article |
AbstractThis article explains how to create your own error objects and messages, and how to process the errors returned by the methods you call using exceptions. Using exceptions is not recommended. |
Suppose you code a method in a matrix class to compute a 3x3 matrix
inversion. If the matrix to invert has a null determinant, you cannot invert the
matrix, and you can report the error by throwing an exception. The code that
will call your method will catch the exception and will process it. Since the
null determinant depends on the input matrix, you will use the CATInputError
class, and create a message such as the one with the key MatrixERR_3241
you will associate with the class, this message being located, for example, in
the file CAAMatrixInputError.CATNls, as follows:
MatrixERR_3241.Request = "Cannot invert the matrix /p1" MatrixERR_3241.Diagnostic = "The matrix determinant is null.\n Its determinant value is: /p2\n This value is less than the lower limit for\n significant real numbers\n and is considered as null."; MatrixERR_3241.Advice = "Check the /p1 matrix"; |
The matrix inversion method can be as follows:
#include "CATInputError.h" #include "CAAMatrix3x3.h" CAAMatrix3x3 * CAAMatrix3x3::Invert() { double det = Determinant(); if (fabs(det) <= _epsilon) { CATInputError* pError = new CATInputError("MatrixERR_3241", // Message key "CAAMatrixInputError"); // Message catalog char * pId = GetMatrixId(); // Prepare parameters for the message CATUnicodeString Param1(pId); CATUnicodeString Param2; Param2.BuildFromNum(det); // Assign parameters to the error pError->SetNLSParameters(2, &Param1, &Param2); pError->SetType(CATErrorTypeCritical); CATThrow(pError); } ... } |
The determinant value computed by the method Determinant
is
compared to the smallest acceptable value _epsilon
and if it is
smaller than this value, a CATInputError class instance is created. Note
that the error class constructor uses the error identifier and the error message
file as arguments. The matrix identifier is retrieved, and is converted into a CATUnicodeString
instance, along with the actual determinant value, and assigned as the
parameters of the error message. They will value /p1
and /p2
in
the error message respectively. The error is also assigned the type CATErrorTypeCritical
.
Then the exception is thrown.
The code to write when you use this method could be the following:
... #include "CATInputError.h" #include "CAAMatrix3x3.h" HRESULT CAAMyClass::Compute() { CAAMatrix3x3 * volatile pMat = NULL; CAAMatrix3x3 * volatile pInvMat = NULL; CATTry { pMat = new CAAMatrix3x3(); ... pInvMat = pMat->Invert(); ... } ... |
Note that the macro CATThrow
does not appear explicitly in the
method Compute
, but in the Invert
method called from Compute
.
You could have used the Invert
method several times in your CATTry
block without to worry about the error it can throw. You simply need to catch
these errors once in CATCatch
macros.
... CATCatch(CATInputError, pOccurringError) { if (NULL != pOccurringError) { ... // Clean const char * pMsgId = pOccurringError->GetMsgId(); if (0 == strcmp("MatrixERR_3241", pMsgId)) // Repair { pOccurringError->Release(); pOccurringError = NULL; ... // Repair the error, for example, by modifying pMat, and go on return CATReturnSuccess; } else if (0 == strcmp("MatrixERR_3242", pMsgId)) // Rethrow as is { ... // Clean CATRethrow; } else if (0 == strcmp("MatrixERR_3243", pMsgId)) // Enrich { pOccurringError->Release(); pOccurringError = NULL; ... // Clean CATInputError * pMyError = new CATInputError("AnalysisERR_1367", "CATAnalysisInputError"); ... // Set message parameters CATThrow(pMyError); } else // Log and rethrow { if (NULL == pInvMat) { ... // Clean ::CATSysLogAbend("This pointer should never be NULL"); CATRethrow; } } } else { ... // Process exceptions with NULL pointers ?? } } CATCatch(CATSystemError, pOccurringError) { ... } ... CATCatchOthers { ... } CATEndTry } |
[Top]
When a failure happens in a method, such as a division by zero, or if input
parameters are out of the scope of the method, exceptions can be used. To do
this, instantiate an existing error class by passing an appropriate error
message key and the error message catalog file name as parameters of the error
class constructor. If the message has parameters, assign them values of matching
variables from your method, and throw the exception. The exception is thrown to
an exception manager with the error class instance attached. Exception managers
are associated with CATTry
blocks and catch the exceptions thrown
along with error class instances and parameters, and processes them.
To make your applications platform-independent, macros are supplied to
implement exceptions rather than using the C++-native try
, catch
,
and throw
expressions. You will have to split your code into blocks
and encapsulate each block using the macros CATTry
and CATEndTry
.
Then you or the methods you call inside the CATTry
block use the
macro CATThrow
whenever an exception is to be thrown. Finally you
use the macros CATCatch
and CATCatchOthers
to write
the error processing code.
Below is an example of such blocks along with the error macros:
... CATTry { ... // a part of your code which contains CATThrow } CATCatch (CATInputError, pError) { ... // code processing the input errors } CATCatch (CATResourceError, pError) { ... // code processing the resource errors } CATCatchOthers { ... // code processing the other errors } CATEndTry ... |
Errors are described using five classes, CATSystemError, CATResourceError,
CATInputError, CATInternalError, and CATCOMErrors which all
derive from the abstract class CATError [2].
Each CATCatch
block applies to exceptions to which a given error
class is attached. The CATCatchOthers
block applies to exceptions
that are not processed by the preceding CATCatch
blocks in the same
CATTry
/CATEndTry
block.
The code encapsulated by the CATTry
macro is your main code to
execute. If during its execution, or during the execution of one of the methods
or functions called from it, an exception occurs and is thrown using the macro CATThrow
,
and if it is not caught by the code you call, the appropriate CATCatch
block embedded in the same CATTry
/CATEndTry
block is
executed, depending on the error class. Then the process resumes after the macro
CATEndTry
.
Each CATCatch
macro has the error class it processes as first
argument. If the error is an instance of the specified class, or if it is an
instance of one of its derived classes, the CATCatch
block for this
class is executed. The second argument contains a pointer to the error class
instance conveyed with the exception.
[Top]
The main characteristics of the error management using exceptions is that an error detected by a method is not returned to its caller, but thrown and may traverse several caller layers before being caught by a caller upwards in the call stack. An exception is built using the following:
Once thrown, the exception traveses the call stack until it finds an
appropriate CATCatch
block. Any caller can catch the exception to
analyze it and take the appropriate decision. Refer to Catching
and Processing Exceptions.
[Top]
When developing your objects and client applications, you will need to create your own errors and may throw exceptions. This includes mainly:
[Top]
This includes:
This is fully described in [2].
[Top]
Exceptions are thrown using the CATThrow
macro and an error
class instance. Use CATThrow
as follows:
... CATThrow(pError); ... |
where pError
is the pointer to the error class instance created
for this exception.
[Top]
Memory leaks with exceptions happen when automatic objects are used. They are allocated on the stack during the execution of a method or a function. The compiler generates calls to their constructors at function entry and to their destructors at function exit. Unlike C++, if an exception is thrown (either in the function itself or by some other functions it called), the call to the destructors will be skipped. The memory taken by these objects will automatically be released as the stack is restored but their destructors will not be called. This may have a serious impact if these destructors were used, for example, to release previously AddRef'd objects.
Consider the following example.
... CATBody * pBody = new Body(); // heap allocation CATDimBoolean * pBool = new CATDimBoolean(); pBool->Run(pBody); ... |
pBody
is allocated on the heap, and passed to the Run
method.
CATDimBoolean::Run(CATBody * iBody) { CATTry { // the constructor of CATTopCheck calls iBody->AddRef(); CATTopCheck Tcheck(iBody); // stack allocation Tcheck.Check(); // may throw an exception } CATCatch(CATError, pError) { ... } } |
The Run method uses iBody to instantiate a CATTopCheck instance as an automatic object on the stack that AddRef's the CATBody. Then the Check method is called, and may throw an exception. In this case, the Tcheck variable is correctly deallocated from the stack, but the CATTopCheck destructor is not called, and the iBody variable is not released. This causes a memory leak.
To avoid memory leaks when such an exception is thrown, declare local variables as volatile beyond the CATTry scope, allocate them on the heap, and explicitly delete or release them when no exception is called, catch the exception and explicitly delete or release them if necessary.
CATDimBoolean::Run(CATBody * iBody) { CATTopCheck * volatile pTcheck = NULL; CATTry { // the constructor of CATTopCheck calls iBody->AddRef(); pTcheck = new CATTopCheck(iBody); // heap allocation pTcheck->Check(); // may throw an exception delete pTcheck; // or Release pTcheck pTcheck = NULL; } CATCatch (CATError, pError) { if (NULL != pTcheck) { delete pTcheck; // or Release pTcheck pTcheck = NULL; } ... //Repair, rethrow, or enrich the exception } ... } |
In other words, avoiding automatic objects prevents from memory leaks with exceptions.
[Top]
If the error is a major one, you must trace it. This is fully described in [2].
[Top]
... /** ... * @exception CATInternalError ComponentERR_1000 * The component cannot be created because no default constructor exists * for the requested class * @exception CATInternalError ComponentERR_1001 * The component cannot be created because the class to instantiate * doesn't exist * @exception CATInternalError ComponentERR_1002 * The component was successfully created, but the interface query fails, * whether the component doesn't implement the interface, or the IID passed is invalid. * An IUnknown pointer to the component is returned * @exception CATInternalError ComponentERR_1003 * The memory allocation failed for the component ... */ ExportedByJS0CORBA virtual void CATInstantiateComponent(const char *iname, const IID &iid, void **oppv); |
which gives after being processed:
public virtual void CATInstantiateComponent(const char *iname, const IID &iid, void **oppv);
Error Class | Error Id | Error Meaning |
---|---|---|
CATInternalError | ComponentERR_1000 | The component cannot be created because no default constructor exists for the requested class |
CATInternalError | ComponentERR_1001 | The component cannot be created because the class to instantiate doesn't exist |
CATInternalError | ComponentERR_1002 | The component was successfully created, but the interface query fails, whether the component doesn't implement the interface, or the IID passed is invalid. An IUnknown pointer to the component is returned |
CATInternalError | ComponentERR_1003 | The memory allocation failed for the component |
[Top]
The exception processing is made using the CATTry, CATCatch, CATCatchOthers and CATEndTry macros. If you catch an exception, your strategy of dealing with this error can be constructed knowing that you can:
When catching an exception you must try in the following order:[Top]
Pay attention that this macro sequence CATTry, CATCatch, CATCatchOthers and CATEndTry is fully respected. If this is not the case, the C++ source generated will be invalid and will not compile. In addition, the error classes used must be one, or derive from one, of the five error base classes [2].
The CATTry macro is used to start a code block to which the exception processing applies. If no error is detected during this code execution, the other macros are skipped and the process resumes after the macro CATEndTry.
Never use a goto
or case
from a switch
statement to enter in a block. The macro CATTry
must always be
executed to ensure proper error processing.
If errors may be encountered, use the macro CATCatch
to process
these errors. Use one macro per error class which can potentially occurs. If
such an error occurs, the macro CATCatch
corresponding to this
error class is executed. The process resumes then after the macro CATEndTry
,
if no other macro CATThrow
or CATRethrow
is found
inside the macro CATCatch
.
#include "CATErrorMacros.h" #include "MyInternalError.h" ... CATTry { Function1 (x); Function2 (y); ... } CATCatch (MyInternalError, pErrorFound) { if (pErrorFound->GetId() == TheErrorIAmWaitingFor) ... } CATCatchOthers { ... } CATEndTry |
The first argument of CATCatch
is the error class, and the
second is a pointer, valued by the code executed inside the macro CATTry
,
to the parameters associated with the error.
The macro CATCatchOthers
is used to insert code for any error
class that is not handled by the macros CATCatch
located in the
same code block. It must be the last of the CATCatch
macro sequence
and must be followed by CATEndTry
. CATCatchOthers
must
be used to protect from exception raise. For example, the preceding macro CATCatchOthers
could be coded as follows:
... CATCatchOthers { ... // clean up cerr << "This error belongs to an unknown class\n; } CATEndTry |
You can replace CATCatchOthers by CATCatch (CATError, Error_Found) since all errors derive from the class CATError.
Never use a goto or a case from a switch statement to enter in a CATCatch block[Top]
This is fully described in [2]. If the error is a major one, you must trace it. This is also described in [2].
[Top]
You often can add information about the error that happens because you have a better knowledge of what you request from the method you call than this method itself. For example, when you ask a method to compute the intersection of two surfaces, this method can simply return, in case of error, that the surfaces cannot be intersected, or that the intersection operator fails. You can replace this error in the calling context, that is for example to create a fillet or a chamfer, release the error class instance, and create a new one that better explains what really happens and that provide useful information to your own caller, or to the end user.
... CATCurve * volatile ptr = NULL; ... CATTry { ... ptr->Intersect(Surf1, Surf2, &Curve); ... } CATCatch(CATError, pError) { ptr->Release(); ptr = NULL; if (NULL != pError) { if pError->IsAKindOf(CATInputError::ClassName()) { if (0 == strcmp("CurveERR_1256", pOccurringError->GetMsgId())) { pError->Release(); pError = NULL; CATInputError * pMyError = new CATInputError("FilletERR_2341", "MsgCatalog"); ... // Add appropriate parameters, if any CATThrow(pMyError); } ... |
[Top]
You can simply resend the error if you cannot do anything, or if your own caller can process it.
To raise again the current exception, use CATRethrow. This is the same as the C++ throw clause with no operand. This macro can only be used inside a CATCatch block.
... CATTry { ... ptr->Intersect(Surf1, Surf2, &Curve); ... } ... CATCatchOthers { ... // Clean CATRethrow; } CATEndTry |
As with CATThrow, the CATTerminate function is called if there is no CATCatch* block to handle this exception.
[Top]
This is fully described in [2].
[Top]
The following table summarizes the differences between the V5 error management and the C++ exceptions
V5 Exceptions | C++ Native Exeptions |
---|---|
Error objects must derive from CATError | Error objects can be of any type |
CATTry |
try |
CATCatch (CATError, pErr) pErr must be a pointer |
catch (CATError, err) err can be passed as a pointer, by reference or value |
CATCatchOthers |
catch(...) |
CATEndTry |
NA |
CATThrow(pErr) pErr must be a pointer |
throw(err) err can be passed as a pointer, by reference or value |
CATRethrow |
throw |
CATDestructOnExit |
NA |
CATTerminate |
terminate |
CATSetTerminate |
set_terminate |
NA | unexpected |
NA | set_unexpected
|
[Top]
Failures in methods or global functions can be reported using exceptions.
Using the macro CATThrow
, an error class instance can be associated
with the exception, along with an error identifier and a error message that can
include parameters values from the method current state. Exceptions can be
traced using the CATSysLogAbend
and CATSysLogError
macros.
Contratry to returned errors, exceptions can traverse the call stack and can be processed by a caller far away from the callee that throws the exceptions. This caller processes the exception, that is, first analyzes it, an then repairs, enriches, or resends it as is. If the method is located at the appropriate level, a prompt can be issued to the end user.
If a method located at the framework border catches exceptions thrown by its callees, it must convert them into HRESULT associated with the same or another error class instance.
[Top]
[1] | CAA V5 Error Processing Rules |
[2] | Using an Error Class with HRESULTs or Exceptions |
[3] | Managing Errors Using HRESULT |
[Top]
Version: 2 [Nov 2001] | Enhanced with new error rules |
Version: 1 [Mar 2000] | Document created |
[Top] |
Copyright © 2000, Dassault Systèmes. All rights reserved.