Thursday, March 30, 2017

Development tutorial: Platform improvements for handling X++ exceptions the right way

A while back I wrote about a pattern we discovered in X++ code which could lead to serious data consistency issues. You can read it again and look at an example mfp wrote up here:
http://kashperuk.blogspot.dk/2016/11/tutorial-link-handling-exceptions-right.html

With the release of Platform update 5 for Dynamics 365 for Operations we should now be better guarded against this kind of issue.

Let's look at the below example (needs to be run in USMF company):

class TryCatchAllException
{
    public static void main(Args _args)
    {
        setPrefix("try/catch example");

        try
        {
            ttsbegin;

            TryCatchAllException::doSomethingInTTS();

            ttscommit;
        }
        catch
        {
            info("Inside main catch block");
        }

        info(strfmt("Item name after main try/catch block: %1", InventTable::find("A0001").NameAlias));
    }

    private static void doSomethingInTTS()
    {
        try
        {
            info("Doing something");
   
            InventTable item = InventTable::find("A0001", true);
            item.NameAlias = "Another name";
            item.doUpdate();

            throw Exception::UpdateConflict;

            // Some additional code was supposed to be executed here
        }
        catch
        {
            info("Inside doSomething catch block");
        }
  
        info("After doSomething try/catch block");

    }

}

Before Platform Update 5 the result would be:


As you can see, we 
  • went into doSomething()
  • executed the update of NameAlias for the item, 
  • then an exception of type UpdateConflict was thrown
  • At this point the catch-all block caught this exception without aborting the transaction, meaning the item is still updated. We did not abort the transaction because we did not think about this case before
  • We exit the doSomething() and commit the transaction, even though we got an exception and did not want anything committed (because the second part of the code did not execute)
  • As a result, the NameAlias is still modified.

Now with Platform Update 5 the result will be:

That is, we

  • went into doSomething(),
  • executed the update of NameAlias for the item,
  • then an exception of type UpdateConflict was thrown
  • At this point the catch-all did not catch this type of exception, as we are inside a transaction scope, so the exception was unhandled in this scope, and went to the one above
  • Since we are by the outer scope outside the transaction, the catch-all block caught the exception,
  • and the NameAlias is unchanged


So, again, we will simply not handle the 2 special exception types (UpdateConflict and DuplicateKey) any longer in a catch-all block inside a transaction scope, you will either need to handle them explicitly or leave it up to the calling context to handle.

This will ensure we do not get into this erroneous code execution path where the transaction is not aborted, but we never handle the special exception types internally.

Hope this helps!