Error and Exception Handling Policy: Difference between revisions

From OpenPetra Wiki
Jump to navigation Jump to search
No edit summary
Line 7: Line 7:
===Overall===
===Overall===
'''Exceptions should be thrown...'''
'''Exceptions should be thrown...'''
* '''whenever genuine Exceptions occur;'''
* '''whenever exceptional conditions occur in a program (e.g. a Method failure or unexpected states of Objects) and the program cannot continue because of this;'''
* '''when other error states (determined by program logic) are detected in code''' and the handling for those is outside the current program scope (e.g. Method, Property Getter/Setter, etc.).  
* '''when other error states (determined by program logic) are detected in code''' and the handling for those is outside the current program scope (e.g. Method, Property Getter/Setter, etc.).  


'''Exceptions ''must not be used'' simply for changing the flow of a program!!!''' Use other control logic (e.g. <code>if</code> or <code>switch</code> statements) for such purposes.
Raising of Exceptions is not considered costly nowadays. We are happy to throw Exceptions across .NET Remoting boundaries even if this costs some resources because the Exceptions need to be Serialized/Deserialized - Exceptions should be thrown in exceptional circumstances only and therefore this should not be a problem.


Raising of Exceptions is not considered costly nowadays. We are happy to throw Exceptions across .NET Remoting boundaries as well, as long this occurs infrequently (see [[Error and Exception Handling Policy#Exception Handling across .NET Remoting Boundaries .28Server_-.3E_Client.29|below]]).


===Practices That Need to be Adhered to===
===Practices That Need to be Adhered to===
====General====
====General====
* '''Exceptions must never be 'swallowed'''', that is, get caught and not get re-raised again!
====='Swallowing' Exceptions - *** DON'T DO IT! ***=====
** What ''is allowed'' is catching of a certain Exception and throwing of another Exception instead (in that Exception handler). However, the new Exception must never be a generalisation. Rather it needs to be a ''specialisation'' of the Exception that gets handled. Alternatively it can be ''a different Exception altogether'', if this is meaningful. The second option needs to be carefully assessed and the pros and cons need to be considered - it must not cause confusion in ''any situation'' where it might be handled by a caller.
'''Exceptions must never be 'swallowed'''', that is, get caught and not get re-raised again! Would this be done, one would let the program carry on as if everything is fine, while in reality an exceptional condition has occurred that would need attention. As a result, the program would likely become unstable and would be left in a non-deterministic state.
*** Example - ALLOWED: catching a <code>System.Exception</code> and raising e.g. a <code>System.IO.IOException</code> or a custom openPETRA Exception (both result in a specialisation, which is fine).
 
*** Example - NOT ALLOWED: catching a <code>System.NullReferenceException</code> and raising a <code>System.Exception</code> instead (that would be a generalisation, in which valuable information is lost!).
=====Catching Exceptions That Result From Programming Errors - *** DON'T DO IT! ***=====
** When another Exception is raised than the original Exception, the ''original Exception '''must''' be added as the 'InnerException' of the newly raised Exception''. In doing so the original Exception information isn't lost (incl. the code line where the original Exception occured [if the Assembly is built in the Debug rather than the Release configuration]!).  
Exceptions which are a result of a programming error shall never be caught, e.g. NullPointerException. This would mask the original error, therefore making it hard to find it.
*** This is done by adding the instance of the original Exception as the second Argument when creating the instance of the new Exception.  
 
**** Example: <code>throw new MyAppException("Error caused by trying ThrowInner.", Exp);</code>, where <code>Exp</code> is the instance of the original Exception.
=====Catching an Exception and Re-raising the Same Exception=====
**** Full Example: see [http://msdn.microsoft.com/en-us/library/system.exception.innerexception(v=VS.80).aspx Exception.InnerException Property]
'''An Exception shall not be caught and then the same Exception be thrown again.'''
 
There are three notable ''exceptions to this rule'', though:
# Logging of the Exception should take place in that very Exception Handler.
# Resources need to be freed in case of an Exception, e.g. DB transaction rolled backed, handles freed, ...
# User Interface update needs to happen that can not be achieved otherwise, e.g. wait cursor resetted, status bar text set, ...
 
=====Raising Another Exception Than The Original Exception (Translating Exceptions)=====
Catching of a certain Exception and throwing of another Exception (possibly being a more precise one) instead in that Exception Handler is allowed.  
 
The new Exception may be ''a different Exception altogether'', if this is meaningful. Throwing different Exceptions should generally be avoided, though, as it makes it harder to see what has originally happened. Use of the 'InnerException' Property (see below) helps in mitigating this, but it can still be tricky to find the root of a problem if Exceptions are 'translated' --- if a different Exception is thrown, the reason for doing so should be carefully assessed and the pros and cons need to be considered, as it must not cause confusion in ''any situation'' in which it might be handled by an Exception Handler in ''any'' position in the Stack.  
 
Throwing of a new Exception which is a generalisation of the original Exception (e.g. catching a <code>System.NullReferenceException</code> and raising a <code>System.Exception</code> instead) is ''strongly discouraged'', as valuable Type information is lost in doing so! (While this could be mitigated by putting the original Exception in the InnerException Property of the new Exception [see below], a generalisation is still making it harder to find the root of a problem as InnerExceptions are not usually evaluated programmatically, but only by a programmer when (s)he is debugging.)
 
'''Use of InnerException Property:''' When another Exception is raised than the original Exception, the ''original Exception '''must''' be added as the ''''InnerException'''' of the newly raised Exception''. In doing so, the original Exception information isn't lost (incl. the code line where the original Exception occurred [if the Assembly is built in the Debug rather than the Release configuration]!).  
* This is done by adding the instance of the original Exception as the second Argument when creating the instance of the new Exception.  
** Example: <code>throw new MyAppException("Error caused by trying ThrowInner.", Exp);</code>, where <code>Exp</code> is the instance of the original Exception.
** Full Example: see [http://msdn.microsoft.com/en-us/library/system.exception.innerexception(v=VS.80).aspx Exception.InnerException Property]
 
=====Use of Custom Exceptions=====
.NET allows the creation of Custom Exceptions.
 
They are useful for several reasons:
* Their Name can tell much more about an Exception situation than a built-in Exception of the .NET Framework;
* They can contain arbitrary additional data;
* It is possible to create custom Exceptions that themselves derive from another Custom Exception. Through that a hierarchy of Custom Exceptions can be created.
 
Type Naming conventions: See [[Coding Standard and Guidelines#Exception Naming Guidelines | Exception Naming Guidelines]]
 
In many cases the creation of a new Custom Exception that has no additional information is sufficient as the Type Name of the new Exception already conveys enough. For Exception handling situations where additional information should be passed to Exception Handlers higher up in the Call Stack, extra data should be added to the custom Exception that is thrown. This helps in handling the Exception because it is possible to process the additional information in Exception Handlers that know about this information.
 
Examples in <code>\csharp\ICT\Petra\Shared\RemotedExceptions.cs</code>:
* Simple Custom Exception: Class '<code>EPagedTableNoRecordsException</code>'
* Custom Exception that holds additional data: Class '<code>ESecurityPartnerAccessDeniedException</code>'
 
======Custom Exceptions crossing .NET Remoting Boundaries======
'''All our Custom Exceptions need to be able to cross .NET Remoting boundaries and ''keep its specific data'' in doing so'''.
All the information that is encapsulated in an Exception is transported without loss of fidelity across .NET Remoting boundaries (this works fine from Linux/mono to Windows/MS .NET as well!).
 
To ensure that Exception-specific data is kept when crossing .NET Remoting boundaries, ''care needs to be taken that any data that is specific to a Custom Exception is not lost during Serialization/Deserialization''! Crucial for that is the correct implementation of the Constructor that has the Arguments 'SerializationInfo' and 'StreamingContext' and of the Method '<code>GetObjectData</code>'.
* Example: <code>\csharp\ICT\Petra\Shared\RemotedExceptions.cs</code>, Class '<code>ESecurityPartnerAccessDeniedException</code>'


====Client-side====
======Custom Exceptions Must Be Specified In Shared Assemblies======
* In a situation where a data saving operation should be aborted programmatically in a ManualCode.cs file because a certain Exception has been thrown, a '''<code>CancelSaveException</code>''' needs to be thrown inside that Exception handler (ref. [[Comments to Revision 1089#A new common Exception: CancelSaveException]]). In doing so, the automatically generated code of the <code>SaveChanges()</code> Method is exited and no data gets saved if code that raises this Exception is called from the <code>GetDataFromControlsManual()</code> Method.
All Custom Exceptions must be specified in DLL's that are shared between Client and Server so that they can be passed from Server to Client (their Type information needs to be available to the Client as .NET Remoting needs to be able to deserialize the exact Type whenever they are thrown on the server side). Therefore Custom Exceptions need to be specified in the Namespace <code>Ict.Petra.Shared</code> or one of its Subnamespaces.
** In such a situation the error handling has to be done inside the Exception handler, an appropriate error message needs to be displayed to the user and then the <code>CancelSaveException</code> needs to be thrown.
*** Example: GLAccountHierarchy.ManualCode.cs, Method 'ChangeAccountCodeValue'. In the try block in specific situations a System.Data.ConstraintException is thrown.


* '''TODO'''


====File Operations====
====File Operations====
'''TODO'''
'''TODO'''


====Server-specific Considerations====
====Server-specific Considerations====
'''TODO'''
=====DB Access=====
=====DB Access=====
At the moment our auto-generated '...SubmitChanges' Methods in the openPETRA Datastore return a TVerificationResultCollection, but that should be changed to Exceptions being thrown instead - see [[DB Discussions: Transaction Model, Savepoints, Locking; Exception Handling; Caching#SubmitChanges_Methods | DB Discussions]]!
At the moment our auto-generated '...SubmitChanges' Methods in the openPETRA Datastore return a TVerificationResultCollection, but that should be changed to Exceptions being thrown instead - see [[DB Discussions: Transaction Model, Savepoints, Locking; Exception Handling; Caching#SubmitChanges_Methods | DB Discussions]]!


====Client-specific Considerations====
'''TODO'''
'''TODO'''


===Exception Handling across .NET Remoting Boundaries (Server -> Client)===
All the information that is encapsulated in an Exception is transported without loss of fidelity across .NET Remoting boundaries (this works fine from Linux/mono to Windows/MS .NET as well!).


We are happy to throw Exceptions across .NET Remoting boundaries. However, this this should be done infrequently as throwing Exceptions across .NET Remoting boundaries is more costly then throwing Exceptions in the same Operating System process.  
====Client-specific Considerations====
Be sure to ''never'' throw Exceptions across .NET Remoting boundaries ''in a loop'' of some sort because this could result in a lot of Exceptions which would all need to be remoted, and this would be quite costly!
=====Aborting a Data Saving Operation=====
In a situation where a data saving operation should be aborted programmatically in a ManualCode.cs file because a certain Exception has been thrown, a '''<code>CancelSaveException</code>''' needs to be thrown inside that Exception handler (see [[Comments to Revision 1089#A new common Exception: CancelSaveException | CancelSaveException]]). In doing so, the automatically generated code of the <code>SaveChanges()</code> Method is exited and no data gets saved if code that raises this Exception is called from the <code>GetDataFromControlsManual()</code> Method.
* In such a situation the error handling has to be done inside the Exception handler, an appropriate error message needs to be displayed to the user and then the <code>CancelSaveException</code> needs to be thrown.
** Example: GLAccountHierarchy.ManualCode.cs, Method 'ChangeAccountCodeValue'. In the try block in specific situations a System.Data.ConstraintException is thrown.
 
'''TODO'''


====Custom Exceptions crossing .NET Remoting Boundaries====
If a custom Exception needs to be able to cross .NET Remoting boundaries and keep its specific data in doing so, care needs to be taken that any custom Exception data is not lost!


Of special importance is the implementation of the Method '<code>GetObjectData</code>': check out some of our custom Exceptions in the Namespace <code>Ict.Petra.Shared.RemotedExceptions</code> to see examples on how this is done properly!
===TODO===
* All OpenPetra-specific Exceptions should derive from a common Base Class XXX.
** The Base Class should add support for I18N of the Exception for showing an error message to the user in various languages.
*** It should be possible to show an I18N exception including the additional data.
** Exceptions should always be logged in English to the log file (no I18N)!
** Introduce specialized Base Classes for Exception for database, IO, server-communication, ... situations. This helps to catch specific areas of expected Exceptions, which could be handled specifically and separately in Exception Handlers.


=====Obligatory Namespace for Remoted Exceptions=====
'''More TODO'''?
All custom Exceptions which can cross .NET Remoting boundaries must be specified in the Namespace <code>Ict.Petra.Shared.RemotedExceptions</code>.





Revision as of 10:18, 3 May 2011

DRAFT DOCUMENT

TODO: This document is a DRAFT and needs to be revised and extended, especially in the areas of Client and Server error handling.

Input should be given on this page.

Exception Handling

Overall

Exceptions should be thrown...

  • whenever exceptional conditions occur in a program (e.g. a Method failure or unexpected states of Objects) and the program cannot continue because of this;
  • when other error states (determined by program logic) are detected in code and the handling for those is outside the current program scope (e.g. Method, Property Getter/Setter, etc.).

Exceptions must not be used simply for changing the flow of a program!!! Use other control logic (e.g. if or switch statements) for such purposes.

Raising of Exceptions is not considered costly nowadays. We are happy to throw Exceptions across .NET Remoting boundaries even if this costs some resources because the Exceptions need to be Serialized/Deserialized - Exceptions should be thrown in exceptional circumstances only and therefore this should not be a problem.


Practices That Need to be Adhered to

General

'Swallowing' Exceptions - *** DON'T DO IT! ***

Exceptions must never be 'swallowed', that is, get caught and not get re-raised again! Would this be done, one would let the program carry on as if everything is fine, while in reality an exceptional condition has occurred that would need attention. As a result, the program would likely become unstable and would be left in a non-deterministic state.

Catching Exceptions That Result From Programming Errors - *** DON'T DO IT! ***

Exceptions which are a result of a programming error shall never be caught, e.g. NullPointerException. This would mask the original error, therefore making it hard to find it.

Catching an Exception and Re-raising the Same Exception

An Exception shall not be caught and then the same Exception be thrown again.

There are three notable exceptions to this rule, though:

  1. Logging of the Exception should take place in that very Exception Handler.
  2. Resources need to be freed in case of an Exception, e.g. DB transaction rolled backed, handles freed, ...
  3. User Interface update needs to happen that can not be achieved otherwise, e.g. wait cursor resetted, status bar text set, ...
Raising Another Exception Than The Original Exception (Translating Exceptions)

Catching of a certain Exception and throwing of another Exception (possibly being a more precise one) instead in that Exception Handler is allowed.

The new Exception may be a different Exception altogether, if this is meaningful. Throwing different Exceptions should generally be avoided, though, as it makes it harder to see what has originally happened. Use of the 'InnerException' Property (see below) helps in mitigating this, but it can still be tricky to find the root of a problem if Exceptions are 'translated' --- if a different Exception is thrown, the reason for doing so should be carefully assessed and the pros and cons need to be considered, as it must not cause confusion in any situation in which it might be handled by an Exception Handler in any position in the Stack.

Throwing of a new Exception which is a generalisation of the original Exception (e.g. catching a System.NullReferenceException and raising a System.Exception instead) is strongly discouraged, as valuable Type information is lost in doing so! (While this could be mitigated by putting the original Exception in the InnerException Property of the new Exception [see below], a generalisation is still making it harder to find the root of a problem as InnerExceptions are not usually evaluated programmatically, but only by a programmer when (s)he is debugging.)

Use of InnerException Property: When another Exception is raised than the original Exception, the original Exception must be added as the 'InnerException' of the newly raised Exception. In doing so, the original Exception information isn't lost (incl. the code line where the original Exception occurred [if the Assembly is built in the Debug rather than the Release configuration]!).

  • This is done by adding the instance of the original Exception as the second Argument when creating the instance of the new Exception.
    • Example: throw new MyAppException("Error caused by trying ThrowInner.", Exp);, where Exp is the instance of the original Exception.
    • Full Example: see Exception.InnerException Property
Use of Custom Exceptions

.NET allows the creation of Custom Exceptions.

They are useful for several reasons:

  • Their Name can tell much more about an Exception situation than a built-in Exception of the .NET Framework;
  • They can contain arbitrary additional data;
  • It is possible to create custom Exceptions that themselves derive from another Custom Exception. Through that a hierarchy of Custom Exceptions can be created.

Type Naming conventions: See Exception Naming Guidelines

In many cases the creation of a new Custom Exception that has no additional information is sufficient as the Type Name of the new Exception already conveys enough. For Exception handling situations where additional information should be passed to Exception Handlers higher up in the Call Stack, extra data should be added to the custom Exception that is thrown. This helps in handling the Exception because it is possible to process the additional information in Exception Handlers that know about this information.

Examples in \csharp\ICT\Petra\Shared\RemotedExceptions.cs:

  • Simple Custom Exception: Class 'EPagedTableNoRecordsException'
  • Custom Exception that holds additional data: Class 'ESecurityPartnerAccessDeniedException'
Custom Exceptions crossing .NET Remoting Boundaries

All our Custom Exceptions need to be able to cross .NET Remoting boundaries and keep its specific data in doing so. All the information that is encapsulated in an Exception is transported without loss of fidelity across .NET Remoting boundaries (this works fine from Linux/mono to Windows/MS .NET as well!).

To ensure that Exception-specific data is kept when crossing .NET Remoting boundaries, care needs to be taken that any data that is specific to a Custom Exception is not lost during Serialization/Deserialization! Crucial for that is the correct implementation of the Constructor that has the Arguments 'SerializationInfo' and 'StreamingContext' and of the Method 'GetObjectData'.

  • Example: \csharp\ICT\Petra\Shared\RemotedExceptions.cs, Class 'ESecurityPartnerAccessDeniedException'
Custom Exceptions Must Be Specified In Shared Assemblies

All Custom Exceptions must be specified in DLL's that are shared between Client and Server so that they can be passed from Server to Client (their Type information needs to be available to the Client as .NET Remoting needs to be able to deserialize the exact Type whenever they are thrown on the server side). Therefore Custom Exceptions need to be specified in the Namespace Ict.Petra.Shared or one of its Subnamespaces.


File Operations

TODO


Server-specific Considerations

DB Access

At the moment our auto-generated '...SubmitChanges' Methods in the openPETRA Datastore return a TVerificationResultCollection, but that should be changed to Exceptions being thrown instead - see DB Discussions!

TODO


Client-specific Considerations

Aborting a Data Saving Operation

In a situation where a data saving operation should be aborted programmatically in a ManualCode.cs file because a certain Exception has been thrown, a CancelSaveException needs to be thrown inside that Exception handler (see CancelSaveException). In doing so, the automatically generated code of the SaveChanges() Method is exited and no data gets saved if code that raises this Exception is called from the GetDataFromControlsManual() Method.

  • In such a situation the error handling has to be done inside the Exception handler, an appropriate error message needs to be displayed to the user and then the CancelSaveException needs to be thrown.
    • Example: GLAccountHierarchy.ManualCode.cs, Method 'ChangeAccountCodeValue'. In the try block in specific situations a System.Data.ConstraintException is thrown.

TODO


TODO

  • All OpenPetra-specific Exceptions should derive from a common Base Class XXX.
    • The Base Class should add support for I18N of the Exception for showing an error message to the user in various languages.
      • It should be possible to show an I18N exception including the additional data.
    • Exceptions should always be logged in English to the log file (no I18N)!
    • Introduce specialized Base Classes for Exception for database, IO, server-communication, ... situations. This helps to catch specific areas of expected Exceptions, which could be handled specifically and separately in Exception Handlers.

More TODO?


Error Handling

Past Implementation and Anticipated Changes

  • In Petra 2.x, a DB Table ('s_error_message') held error codes and the associated error messages centrally. A central function existed which displayed these errors when the software developer supplied the error code.
  • In openPETRA we will probably do this differently - a global static Class which holds error codes is envisaged, and a message library which will display errors.

Error Codes

See Error Codes in Openpetra.

Central Place for Maintenance of Unique Error Codes

The goal is to have a numbering scheme for unique error codes that

  • allows new error codes to be introduced easily;
  • ensures that error codes are used and re-used appropriately by the software developers;
  • distinguishes between data verification errors and exceptions.

See Central Inventory of openPETRA Error Codes.