Monday, April 15, 2013

Exception Handling in Dynamics AX

Exception handling in Dynamics AX is a topic that is not discussed too often. I figured I would provide a quick musing about some of my favorite exception handling topics.

Database Transactions
Exception handling while working with database transactions is different. Unfortunately, not a lot of people realize this. Most exceptions cannot be caught within a transaction scope. If you have a try-catch block within the ttsBegin/ttsCommit scope, your catch will not be used for most types of exceptions thrown. What does happen is that AX will automatically cause a ttsAbort() call and then look for a catch block outside of the transaction scope and execute that if there is one. There are however two exception types you CAN catch inside of a transaction scope, namely Update Conflict and Duplicate Key (so don't believe what this MSDN article says). The reason is that it allows you to fix the data issue and retry the operation. You see this pattern in AX every now and then, you have a maximum retry number for these exceptions, after which you throw the "Not Recovered" version of the exception. The job below shows a generic X++ script that loops through each exception type (defined by the Exception enumeration), throws it, and tries to catch it inside a transaction scope. The output shows if the exception is caught inside or outside the transaction scope.

static void ExceptionTest(Args _args)
{
    Exception exception;
    DictEnum dictEnum;
    int enumIndex;

    dictEnum = new DictEnum(enumNum(Exception));
    for (enumIndex=0; enumIndex < dictEnum.values(); enumIndex++)
    {
        exception = dictEnum.index2Value(enumIndex);
        try
        {
            ttsBegin;

            try
            {
                throw exception;
            }
            catch
            {
                info(strFmt("%1: Inside", exception));
            }

            ttsCommit;
        }
        catch
        {
            info(strFmt("%1: Outside", exception));
        }
    }
}


Fall Through
Sometimes you just want to catch the exception but not do anything. However, an empty catch block will result in a compiler warning (which of course we all strive to avoid!). No worries, you can put the following statement inside your catch block:

Global::exceptionTextFallThrough()

Of course, you're assuming the exception that was thrown already provided an infolog message of some sort. Nothing worse than an error without an error message.


.NET Interop Exceptions
When a .NET exception is thrown, they are typically "raw" exceptions compared to our typical 'throw error("message here")' informative exceptions. I've seen quite a lot of interop code that does not even try to catch .NET call exceptions, let alone handle them. The following examples show different tactics to show the actual .NET exception message. Note that not catching the error (trying to parse "ABCD" into an integer number) does not result in ANY error, meaning a user wouldn't even know any error happened at all.

Strategy 1: Get the inner-most exception and show its message:
static void InteropException(Args _args)
{
    System.Exception interopException;
    
    try
    {
        System.Int16::Parse("abcd");
    }
    catch(Exception::CLRError)
    {
        interopException = CLRInterop::getLastException();
        while (!CLRInterop::isNull(interopException.get_InnerException()))
        {
            interopException = interopException.get_InnerException();
        }
        
        error(CLRInterop::getAnyTypeForObject(interopException.get_Message()));
    }
}


Strategy 2: Use ToString() on the exception which will show the full stack trace and inner exception messages:
static void InteropException(Args _args)
{
    System.Exception interopException;
    
    try
    {
        System.Int16::Parse("abcd");
    }
    catch(Exception::CLRError)
    {
        interopException = CLRInterop::getLastException();
        
        error(CLRInterop::getAnyTypeForObject(interopException.ToString()));
    }
}


Strategy 3: Get all fancy and catch on the type of .NET exception (in this case I get the inner-most exception as we previously have done). Honestly I've never used this, but it could be useful I guess...
static void InteropException(Args _args)
{
    System.Exception interopException;
    System.Type exceptionType;
    
    try
    {
        System.Int16::Parse("abcd");
    }
    catch(Exception::CLRError)
    {
        interopException = CLRInterop::getLastException();
        while (!CLRInterop::isNull(interopException.get_InnerException()))
        {
            interopException = interopException.get_InnerException();
        }
        
        exceptionType = interopException.GetType();
        switch(CLRInterop::getAnyTypeForObject(exceptionType.get_FullName()))
        {
            case 'System.FormatException':
                error("bad format");
                break;
            default:
                error("some other error");
                break;
        }
    }
}


throw Exception::Timeout;


4 comments:

  1. thanks for another good read. you are correct, there isn't enough discussion about this critical topic. I have run across some tricky end user error reporting, in particular trapping AIF and GAB errors.

    ReplyDelete
  2. Just few thoughts:
    1) I wouldn't display the deepest inner exception only, usually the most usefuk is either the highest exception (except TargetInvocationException) or the hierarchy of exceptions.
    2) You can also use AifUtil::getClrErrorMessage().
    3) In the third exaple, I would use "is" operator (AX2012):
    if (interopException is System.FormatException) {...}
    4) Anything more complex (exception filters, disposable types etc.) is much easier directly .NET; it's often wise to implement as much as possible directly in .NET and leave only simply catch in X++.
    5) I strongly recommend to write APIs that don't expose any unhandled CLR exception to the caller code - you can't trust that somebody else will handle it.

    ReplyDelete
  3. This helped me find an issue today! I was thinking it would go to the closest catch. Thank you!

    ReplyDelete