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.
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:
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:
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
.
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.
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.
18.225.235.144