Freigeben über


Exception handling and transactions

The runtime semantics of exceptions is peculiar in X++. The difference from the semantics from other languages is that exception handling considers any catch blocks that are in a scope that contains a running transaction as ineligible to handle the exception. Consequently the behavior of exceptions is quite different in the situation where a transaction is running and in the situation where no transactions are running. This makes the exception semantics different from exception semantics of other languages, even though the syntax is very much the same. This is a perfect setup for confusion even for seasoned X++ programmers.

Please consider the example below for the following discussion:

  1: try 
 2: { 
 3:     MyTableType t; 
 4:     ttsBegin;    // Start a transaction 
 5:     try  
 6:     { 
 7:         throw error(“Something bad happened”); 
 8:     } 
 9:     catch 
10:     { 
11:         // Innermost catch block 
12:     } 
13:     update_recordset t … // Must run within a transaction 
14:     ttsCommit;    // Commit the transaction 
15: } 
16: catch 
17: { 
18:     Info (“In outermost exception handler”); 
19: } 
20:  
20: 

When the exception is thrown in the code above, the control is not transferred to the innermost catch block, as would have been the case if there had been no transaction active; Instead the execution resumes at the outermost exception handler, i.e. the first exception handler outside the try block where where the transaction started (This is the catch all exception handler in line 19).

This behavior is by design. Whenever an exception is thrown all transactions are rolled back (except for two special exceptions namely Exception::UpdateConflict and Exception::UpdateConflictNotRecovered, that do not roll back the active transactions). This is an inseparable part of the semantics of throwing exceptions in X++. If the exception is not one of the two exceptions mentioned above, the flow of control will be transferred to the handler of the outermost block that contains a TTSBegin. The reasoning behind this is that the state of the database queries would not be consistent when the transactions are rolled back, so all code potentially containing this code is skipped.

Another issue is balancing of the transaction-level.

Please consider an amended example:

  1: try 
 2: { 
 3:     MyTableType t; 
 4:     ttsBegin;      // Start a transaction – Increment TTS-level to 1 
 5:     try  
 6:     { 
 7:         ttsBegin;  // Start a nested transaction – Increment TTS-level to 2 
 8:         throw new SysException(“Something bad happened”); 
 9:         ttsCommit;     // Decrease TTS-level to 1 
10:     } 
11:     catch 
12:     { 
13:         // Innermost catch block 
14:         // What is the tts level here? 
15:         info (appl.ttslevel()); 
16:         // Something bad happened -> abort the transaction 
17:         ttsAbort; 
18:     } 
19:     update_recordset t … // Must run within a transaction 
20:     ttsCommit;       // Decrease TTS-level to 0, and commit 
21: } 
22: catch 
23: { 
24:     Info (“In outermost exception handler”); 
25: } 

Let’s examine the case where we allowed the inner catch block to run as the result of throwing the exception. The question is: What should the TTS-Level be in the innermost catch block?

TTSlevel is 1: This won’t work, as the innermost catch block must be able to compensate (and commit) or abort the level-2 transaction. TTS Level 1 would mandate that the system has taken the decision to either abort or commit the level-2 transaction.

TTSlevel is 2: This won’t work either, as the innermost catch block should be able to abort the level-2 transaction. A ttsabort inside the innermost catch block will also abort the level-1 transaction, and render the code following the catch block void. This in turn requires that the innermost catch block throws a new exception, and then we end up in the outermost catch block anyway.

I hope this provides a little clarity in these murky waters.

Comments

  • Anonymous
    September 10, 2007
    The comment has been removed

  • Anonymous
    September 26, 2007
    So.. In 5.0 this isn't going to be "fixed"? This makes absolutely no sense. I have had so many issues with this. There are times when an exception is thrown, but it is not a bad exception.  My example is when we were checking to see if a specific user exists in the current AD Domain. This function throws and exception if the user doesn't exist, but since this was in a nested TTS block, it basically percolated all the way up as you said. This is so different than other languages. Why can't this be fixed? Anytime I teach DEV 2 and have to get to exception handling and they have extensive experience with other languages, they just shake their heads and say "Why has Microsoft implemented this contrary to every other language...". I also warn them of this issue, since I have been burnt badly by it in the past.

  • Anonymous
    September 26, 2007
    Also, MyTable  t;  in the TRY block? Is this something new as well?

  • Anonymous
    October 02, 2007
    The comment has been removed

  • Anonymous
    October 13, 2007
    Dear pvillads, As much as I like the transactional support in the core of the language, as much I dislike the connection between exception handling and transactions. I do agree with you that transaction scope should be part of the syntax and have syntax based scope. (Hope you realize that by disregarding the inner try-catch blocks you make transactions being syntax dependant) Here is what I suggest:  ban the standalone ttsbegin and ttscommit and replace it with special try-catch syntax: try(new Connection()) {   //implicit TTSbegin }   //implicit TTScommit catch {   //implicit TTSabort. } Both Oracle and MSSQL2005 support the nested transactions, and that would be ideal opportunity to use it. conn = new Connection(); try(conn) {   //implicit TTSbegin = BEGIN TRANS LEVEL1     try(conn)     {  //implicit TTSbegin = BEGIN TRANS LEVEL2          SalesFormLetterInvoice::post(SalesTable);     }  //implicit TTScommit = COMMIT TRANS  LEVEL2     catch(Error)     {   //implicit TTSabort  = ROLLBACK TRANS LEVEL2          if(..cannot compensate the error...)              throw Error(....)          else              compensate the error.     } }   //implicit TTScommit =  COMMIT TRANS LEVEL1 catch {   //implicit TTSabort = ROLLBACK TRANS LEVEL1 } That way each try-catch block is eligible to work, and each of them operates own (sub) transaction. It is also much less demanding on the developer, preventing  improper or unbracketed use of ttsbegin and ttscommit. Normal (non db) try-catch bloack work as usually, allowing to catch X++ and CLR exceptions without unwanted rollbacks. This also would solve your doubts about the ttslevel in catch() block. :-) Please let me know what do you think. Michal Kupczyk

  • Anonymous
    January 15, 2008
    I like your solution Michael as it's similar to using (TransactionScope ts = new TransactionScope()){} in c# Rob McIntyre

  • Anonymous
    January 05, 2009
    Un ejemplo fácil de como agregar gestión de excepciones a nuestro código dentro de AX. Lo vamos a hacer

  • Anonymous
    July 09, 2010
    Why is this not fixed in 6.0? We have added to many enhancements to MorphX and X++ why not fix this?