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 (8)

  1. Joris says:

    is that line

    throw new SysException(“Something bad happened”)

    a hint of what’s to come? 🙂

  2. Michael.Franchino@mcaconnect.net says:

    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.

  3. Michael.Franchino@mcaconnect.net says:

    Also, MyTable  t;  in the TRY block? Is this something new as well?

  4. msmfp says:

    Joris: Yes, we have a prototype of class based exceptions implemented – but it does not make it into 5.0.

    Michael: I understand your confusion on this matter, and I fully appreciate the challenge of educating others in this area – I had the same challenge when writing Inside Dynamics AX 4.0.

    However, this is NOT contrary to any other language (that I’m aware of). I do not know of any other language that have embedded transaction support. This blog post actually tries to explain why an exception logically cannot be caught inside a transaction (with the current transaction framework in X++).

    The question to answer is: What should the transaction level be inside the catch-block? As neither “unchanged” or “minus-1” works (1 or 2 in the blog examples),  the only solution is to prevent execution inside the transaction = rollback and catch outside.

    So what does other languages do? In C# you will find transaction APIs. This is a different approach to the problem. Using an API is more flexible, but also more demanding of the developer. Suppose transactions and exceptions were totally decoupled in X++. Consider what additional code is required to avoid unbalanced transactions and ensure data consistency. And consider the impact on all the business logic (Microsoft’s/partner’s/ISV’s/customer’s) if we changed the approach.

    MyTable t; … I believe this was just an artifact of the author’s convenience 🙂

  5. michalkupczyk says:

    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

  6. rmcintyre says:

    I like your solution Michael as it’s similar to

    using (TransactionScope ts = new TransactionScope()){}

    in c#

    Rob McIntyre

  7. Dynamics AX says:

    Un ejemplo fácil de como agregar gestión de excepciones a nuestro código dentro de AX. Lo vamos a hacer

  8. Tubu says:

    Why is this not fixed in 6.0? We have added to many enhancements to MorphX and X++ why not fix this?

Skip to main content