Exception handling

Throughout the book, we have raised many exceptions through the throw command, but so far, we have simply allowed this exception to fall through the code, giving our user an error.

This is fine for many cases, but sometimes, we need specific handling. This is especially important when the error is a number of classes deep, and could produce an error message that makes no sense to the user. We may, in these cases, need to provide some context for the message or a different, more useful message for the user.

Handling general exceptions

In the previous section, an error could have been produced in many places, none of which are useful to the user. They are useful to the administrator or developer.

Looking at the code the errors we might expect are as follows:

  • Invalid field ID in the control definition contract
  • Class does not implement the interface
  • Error thrown by the class that implements the interface

We do not, in any case, want an error to be thrown that stops the form from opening. Also, if there is an error, we need to decide whether the user actually needs to know that an error occurred. It may be enough for our purposes that the fields don't appear, and we can use the debugger to trace through the code to determine why.

In our ConFMSVehicleFieldGroupUIBuilder class, we throw an error in getControlList should the class not exist or not implement the interface. This will stop processing the code, issue a ttsabort command, and result in the vehicle form not being displayed. All we want to do is to skip the class.

The call stack buildUI calls buildUIForClass for each class in the setup table. The buildUIForClass class will call getControlList and pass the list to addControls. We therefore want to catch any errors thrown in getControlList, or anywhere down the stack triggered while adding the controls, and proceed to the next class. It is merely drawing the interface, and we are not performing a transactional update that requires the entire update to succeed or fail.

We would try and catch the error within the getControlList method, as the code in buildUIForClass would still continue. We need to try getControlList and buildUIForClass and catch any errors with a suitable message.

Update the method buildUIForClass to read like this:

private void buildUIForClass(ClassId _classId)
{
    List list;
    try
    {
        list = this.getControlList(_classId);
        if(list.elements() != 0)
        {
            this.addControls(_classId, list);
        }
    }
    catch
    {
        warning("Not all controls were added.");
    }
}

To test this, add the following line at the end of getControlList:

throw error("Confusing technical error.");

On running the form, we get the following message:

Handling general exceptions

Our catch clearly caught the exception and the code continued (the form opened), but we still got a confusing technical error. This is sometimes desirable, but what do we do if we want to hide this message from the user?

We could clear the infoLog object of messages just before we add our warning to it, but this will clear all messages, including those created by other processes. The info log is global, so we need to be careful not to remove messages that we didn't create. The infoLog object has a method called cut that lets us remove log entries from it, so we simply store the current infoLog line at the start of our routine and clear the entries after that. We can amend the buildUIForClass method as follows:

private void buildUIForClass(ClassId _classId)
{
    List list;
    int infoLogLineBefore = infologLine();
    try
    {
        list = this.getControlList(_classId);
        if(list.elements() != 0)
        {
            this.addControls(_classId, list);
        }
    }
    catch
    {
        infolog.cut(infoLogLineBefore+1, infologLine());
        warning("Not all controls were added.");
    }
}

Within the catch block, we can write almost any cleanup code, but if this code fails, there is no fallback to handle errors within the catch block; the code in the catch block must be simple and not have any risk of failure. So, writing a log file or log database entry is not a good idea; currently, we don't have a finally clause in X++ to handle failures in the catch.

Handling CLR exceptions

The catch in the preceding example will catch all types of exceptions. Sometimes, we need to be more specific. Two common examples are handling CLR errors and database update errors.

If we call .NET code from AX, the error will not be passed to the infoLog object; we must get the CLR exception.

A catch statement for getting the CLR exception details is as follows:

    System.Exception outerEx, innerEx;
    try
    {
        //CLR Code
    }
    catch (Exception::CLRError)
    {
        outerEx = CLRInterop::getLastException();
        if(outerEx != null)
        {
            innerEx = outerEx.get_InnerException();
            error(strfmt("%1 
 %2", 
                  outerEx.get_Message(),
                  innerEx.ToString()));
        }
    }
    catch
    {
        warning("<suitable message on general failure>.");
    }

The exception has a lot of details available. You should explore this with your own CLR code to see how to handle CLR errors.

The article How to: Catch Exceptions Thrown from CLR Objects [AX 2012] provides more information on how to catch the exception. You can find this document at http://msdn.microsoft.com/en-us/library/ee677495.aspx.

Handling table update exceptions

There are two main table update exceptions, UpdateConflict and DeadLock. An update conflict occurs due to the optimistic concurrency failing, whereas a deadlock is the classic database scenario where both transactions have each locked a table that the other needs.

Deadlocks should be rare in properly normalized database designs, where the transactions are kept as short as possible, but still they can occur. Deadlocks happen when we are frequently performing a transaction that affects many tables or where one or more tables are being updated by another routine that affects many tables. Examples of these are inventory journals, where one update will affect inventory transactions, inventory on hand, financial ledgers, and so on.

AX will handle the deadlock all by itself. The losing transaction will be rolled back, and the user will be given a message that is normally given straight to IT support for them to usually say "Try again."

We can, however, try again for the user with the retry option. The following code demonstrates this pattern:

static void ConFMSDeadLockExample(Args _args)
{
    boolean firstTime = true;
    
    try
    {
        ttsBegin;
        //post transaction
        ttscommit;
    }
    catch (Exception::Deadlock)
    {
        retry;
    }
}

Update conflicts are normally handled within the insert, delete, and update methods of a table. The BOM table is a good example of this. You may also find it hard to find many examples where this has been used. We use this pattern only if we deem it to be required. The code within the table's update method also updates other records, so it has been written to handle update conflicts.

The following code is an example pattern:

public void update()
{
    #OCCRetryCount
    try
    {
        ttsbegin;
        // code that updates records in other tables
        super(); // do the update
        // other code that updates records in other tables
        ttscommit;
    }
    catch (Exception::Deadlock)
    {
        retry;
    }
    catch (Exception::UpdateConflict)
    {
        if (appl.ttsLevel() == 0)
        {
            if (xSession::currentRetryCount() >= #RetryNum)
            {
                throw Exception::UpdateConflictNotRecovered;
            }
            else
            {
                retry;
            }
        }
        else
        {
            throw Exception::UpdateConflict;
        }
    }
}

It is important that we don't retry indefinitely, as this may cause the client to hang. To control this, we use xSession::currentRetryCount() to get the number of retries and check this against the #RetryNum macro. The macro defines the standard number of retries deemed appropriate by Microsoft, which is five.

The other new element here is ttsLevel. Since transactions can be nested, we do want the exception to fall through to the parent transaction if one exists. If ttsabort is issued (directly or due to a throw error) at any level, the whole transaction will be rolled back; we can't roll back just the level where the error is thrown.

See Exception handling with try and catch Keywords [AX 2012] from Microsoft for further reading, at http://msdn.microsoft.com/en-us/library/aa893385.aspx.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.235.144