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:
3: MyTableType t;
4: ttsBegin; // Start a transaction
7: throw error(“Something bad happened”);
11: // Innermost catch block
13: update_recordset t … // Must run within a transaction
14: ttsCommit; // Commit the transaction
18: Info (“In outermost exception handler”);
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:
3: MyTableType t;
4: ttsBegin; // Start a transaction – Increment TTS-level to 1
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
13: // Innermost catch block
14: // What is the tts level here?
15: info (appl.ttslevel());
16: // Something bad happened -> abort the transaction
19: update_recordset t … // Must run within a transaction
20: ttsCommit; // Decrease TTS-level to 0, and commit
24: Info (“In outermost exception handler”);
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.