In this article, learn how to add exception handling by using D365 try catch statements. Unexpected errors and exceptions can occur in any program. Without exception handling, when an error occurs the system will stop the process entirely. However, with the right code, you can ensure the system skips past invalid records and continues to process all records that are valid. In Microsoft Dynamics 365 for Finance and Operations there are a few different ways to use try catch blocks. By the end of this article, you will understand how to write them in your own code.
Basic Try Catch Block
What is a D365 try catch statement? A try catch statement is x++ code that works similar to an ‘if else’ statement. Whenever an error occurs while running code INSIDE the ‘try’ statement’s curly braces, the system will start running the code inside of the ‘catch’ statement’s curly braces.
In the example below, only the bold text will be run by the system.
try
{
info("Start process");
//Pretend an unexpected error occurs.
//Such as a divide by zero error.
throw Error("An error occurred");
//The system will now start running code inside the 'catch' block, and NOT continue
//The system will not get here.
info("End process");
}
catch
{
info("Additional code can be run when an error occurs");
}
The following text will be displayed:
Start process
An error occurred
Additional code can be run when an error occurs
Types Of Messages
Most of the time, you do not actually need a D365 try catch statement. Typically, developers will write code that specifically display a message when an issue has been detected.
Specifically, there are three types of messages that are shown to a user.
- Info – This global function is used to show informational messages to the user. Notice, these messages are shown with a blue background in the browser.
- Warning – This global function is used to warn the user of potential issues that they should consider correctly, but are not critical. Notice, these messages are shown with a yellow background in the browser.
- Error – This global function is used to notify the user that an error has occurred. As a best practice, an error indicates that the system has not completed the task it was asked to do. Notice, these messages are shown with a yellow background in the browser.
It is important to understand that Info and Warning functions are used in code to share information with the user. However, when a developer uses the error function, they should use the ‘throw’ keyword. This tells the system to stop running any further code, and display the message to the user.
See this example code:
public static void BasicMessages()
{
boolean validationError = true;
boolean warning = true;
info("This is a normal informational message that shows with a blue background");
if (warning)
{
warning("This is a warning message that shows with a yellow background");
warning("As a best practice, warnings do not stop the flow of code");
}
if (validationError)
{
throw Error("Errors show with a red background. As a best practice errors should use the throw keyword which will stop the process.");
}
info("No validation errors occured, so we can proceed with the process.");
//Add code ro run process here.
}
Notice, this error message is being shown to the user when a certain situation is detected. In this example, the validationError boolean is true.
Ultimately, it is most helpful to the end user to write code that detects issues, and displays messages that explain what the user needs to do.
When To Use a Try Catch Statement
So, when should a developer use a D365 try catch statement?
Unexpected Errors
Typically, when the system runs code, if an error occurs that is unexpected, such as a divide by zero error, or a null reference exception, the system will stop running the code. This means that the work the code was trying to perform does not complete.
Additionally, the user may not receive an error message letting them know why the processed stopped. Also, the user will not necessarily be told in what part of the process the exception occurred.
Even if user does receive some kind of message, it is not always clear what the actual issue is.
Ideally, developers could add code to detect and display specific error messages for every possible thing that could go wrong. However, this is just not possible. Not all errors can be anticipated by the developer.
Display Information And Continue The Process
D365 try catch statements all developers to still provide messages and actions even when an error occurs in a block of code, without knowing ahead of time the specific reason for the error.
Overall, the purpose of a try catch statement is to allow a developer to run additional code when an unexpected exception occurs. While any code can be written when the error occurs, these are the most common actions.
- Most commonly, a developer will display a message providing more information to the user as to what the system was doing when the error occurred.
- Additionally, any incomplete work performed can be rolled back to a known state that is ready to be retried. Any data changes that occurred inside ttsbegin and ttscommit statement will be rolled back when the catch block is run.
- And lastly, catching the error, allows the system to continue processing other records. See the example a little later.
To better understand, let us look at an example.
Exception Handling Use Cases
When the system runs a process and there is not a D365 try catch block, the system will still try to show an error message to the screen. And this message is often enough information.
Therefore, adding a D365 try catch block is typically helpful in these two situations.
Additional Information
First, since the error is unexpected, adding a ‘catch’ statement is only helpful if the developer can display more information that what the system would already display to the screen.
When a lot of code is run during a process, adding a try catch block can help provide a user and developer more information as to where in the code the issue occurred. For example, what method the error occurred in.
Additionally, the message in the catch statement can display information about what record was being processed when the error occurred. Definitely, use the global function strFmt.
try
{
info(strFmt("Processing item %1", inventTable.ItemID));
if (validationError)
{
throw Error(strFmt("Error occured wile processing item %1", inventTable.ItemID));
}
info(strFmt("No validation errors occured on item %1, so we can proceed with the process.", inventTable.ItemID));
//Add code ro run process here.
}
catch
{
error(strFmt("An error occurred while processing itemId %1.", inventTable.ItemID));
}
Allow The Process To Continue
Secondly, often times code is written to loop through records and perform work on each record. There are times when if a single record fails to complete, the whole process should stop. However, other times, if a single record fails to complete, the system should continue to process all of the remaining record.
In this second scenario, a D365 try catch block allows the process to continue. Consider the following example:
public static void BasicTryCatchWithLoop()
{
InventTable inventTable;
boolean validationError = true;
while select inventTable
{
try
{
info(strFmt("Processing item %1", inventTable.ItemID));
if (validationError)
{
throw Error(strFmt("Error occured wile processing item %1", inventTable.ItemID));
}
info(strFmt("No validation errors occured on item %1, so we can proceed with the process.", inventTable.ItemID));
//Add code to run process here.
}
catch
{
error(strFmt("An error occurred while processing itemId %1.", inventTable.ItemID));
}
}
}
Whenever there is an error processing a specific inventTable record, the system will not stop processing. Instead, the system will run the code in the ‘catch’ block, then continue on looping through the records.
At the very end, the user will see messages for every record that experienced an error. Additionally, since we used the strFmt function to display the itemId of the record, the user will know exactly which record needs investigating.
To emphasize, without the try catch block the process would have stopped after the first error. The user could then correct the record, and run the process again until the next error occurred. Repeating over and over again until all the records were successfully processed.
Ultimately, using a D365 try catch statement allows the system to successfully process all records it can, and then provide a single complete list of records that require correcting.
Different Types of Exceptions
Earlier, I said that a try catch statement is kind of like an ‘if else‘ statement. With a basic ‘try catch‘ statement, any error message will go into the ‘catch‘ statement. However, there are several different types of exceptions that can be detected. Look at this example code.
public static void CatchDifferentTypesOfErrors()
{
System.String netString = "Net string.";
System.Exception netExcepn;
try
{
info("In the 'try' block. (j3)");
netString.Substring(-2); // Causes CLR Exception.
}
catch (Exception::Error)
{
info("Caught 'Exception::Error'.");
}
catch (Exception::CLRError)
{
info("Caught 'Exception::CLRError'.");
netExcepn = CLRInterop::getLastException();
info(netExcepn.ToString());
}
catch (Exception::Deadlock)
{
}
}
Depending on what type of error has occurred, the system will begin running code in a different ‘catch’ statement. This helps the developer display different messages depending on what type of error has occurred.
For example, when a ‘CLRError’ occurs, the function ‘CLRInterop::getLastException()’ can be used to display the last exception.
Or, when a deadlock exception occurs, the developer can write code to ‘retry’ the process again.
See the full list of types of exceptions here.
Ultimately, using ‘catch‘ without an exception type will catch all types of exceptions.
Deadlocks, Update Conflicts
Although many exceptions will occur until you fix the data or the code, there are some exceptions that are temporary.
For example, there are times when another process will update records on a table. While this is occurring, the system will not let your process update the same records. The current process will wait until the other process is finished before running its own update.
Typically the wait time is short. However, if the system continues to wait for a long time, the system will throw a deadlock or update conflict exception. See below for their specific definitions.
- Deadlock – Microsoft defines a deadlock this way: “A deadlock occurs when two or more database tasks permanently block each other by maintaining a lock on a resource that the other tasks are attempting to lock.”
- Update conflict: – Due to another user process deleting the record or changing one or more field in the record.
Retry
Often times, the user should just try the process later. However, there are some scenarios, such as during a multi-threaded batch job, where a developer may expect some deadlocks to occur.
In those cases, developers should add the ‘retry‘ statement inside a ‘catch‘ statement. This tells tells the system to start running code at the beginning of the ‘try‘ block, and try again. See the following example.
public static void TryCatchRetry()
{
boolean deadlockOccured = true;
int deadlockCounter = 0;
try
{
//run some process that has a chance of having a deadlock.
//A deadlock is when a record can't be modifed because another process
//is using it for a while.
//The system will give up waiting for the record after some time.
//However, using a try catch, we can tell it to essentially try again.
if (deadlockOccured)
{
throw Error("A deadlock occured");
}
}
catch
{
if (deadlockCounter < 5)
{
deadlockCounter++;
//wait one second and try again from the beginning of the 'try'.
sleep(1000);
retry;
}
else
{
//if after 5 retries, the record is still not available
//stop trying and throw an error to let the user know.
throw Error("There is still a deadlock after retrying 5 times. Try again later.");
}
}
Info("End of process");
}
Notice, since I cannot easily simulate a deadlock, I am just throwing regular errors. In the above code, I tell the system to wait for one second, then retry. If the system still is not successful after 5 retries, the system throws an error message.
For a more realistic example, see the code in the DMFStagingCleanup class.
public void run()
{
#OCCRetryCount
try
{
ttsbegin;
if (! this.validate())
{
throw error("@SYS18447");
}
this.deleteStagingAsync();
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;
}
}
}
Notice, the system uses the function ‘xSession::currentRetryCount()‘ to determine how many times it has retried. And, if too many, the system throws an error instead of retrying again.
Nested Try Catch
Interestingly, D365 try catch statements can be nested. Whenever an error occurs inside of a ‘try’ block, the system will continue running code in the corresponding ‘catch’ block.
See the following example code:
public static void NestedTryCatch()
{
try
{
try
{
try
{
throw error("Throwing exception inside Catch 1.");
}
catch (Exception::Error)
{
info("Catch_1");
}
throw error("Throwing exception inside Catch 2.");
}
catch (Exception::Error)
{
info("Catch_2");
}
throw error("Throwing exception inside Catch 3.");
}
catch (Exception::Error)
{
info("Catch_3");
}
info("End of job.");
}
To explain, the system will display the following message:
Throwing exception inside Catch 1.
Catch_1
Throwing exception inside Catch 2.
Catch_2
Throwing exception inside Catch 3.
Catch_3
End of job.
However, this is not always true when using a transaction block.
Nested Try Catch With A Transaction Block
While most of the time, nested D365 try catch blocks work the way you would expect, there is an exception. (No pun intended). The system will not go into a ‘catch’ block when it is also INSIDE of a ttsbegin and ttscommit transaction block.
Instead, the system will go to the innermost ‘catch’ that is OUTSIDE of the transaction block. Look at this example. Only the bolded text will be run.
public static void NestedTryCatchWithTransactionBlock()
{
InventTable InventTable;
/***
Shows an exception that is thrown inside a ttsBegin - ttsCommit
transaction block cannot be caught inside that block.
***/
try
{
try
{
ttsbegin;
while select InventTable
{
try
{
throw error("Throwing exception inside transaction.");
}
catch (Exception::Error)
{
info("Catch_1: Unexpected, caught in 'catch' inside the transaction block.");
}
}
ttscommit;
}
catch (Exception::Error)
{
info("Catch_2: Expected, caught in the innermost 'catch' that is outside of the transaction block.");
}
}
catch (Exception::Error)
{
info("Catch_3: Unexpected, caught in 'catch' far outside the transaction block.");
}
info("End of job.");
}
To explain, when the error is thrown inside of the ‘while select’ loop, the system will NOT go into the ‘catch’ statement. Instead, the system will run the second catch statement.
The messages shown will be:
Throwing exception inside transaction.
Catch_2: Expected, caught in the innermost 'catch' that is outside of the transaction block.
End of job.
Finally
Finally, pun intended, there is one more optional block that can be used with D365 try catch blocks. Use the keyword ‘finally‘. Code in the finally block will run last, regardless of whether an exception occurred or not.
Wee the below example:
public static void TryCatchFinally()
{
try
{
info("Start process");
//Pretend an unexpected error occurs.
//Such as a divide by zero error.
throw Error("An error occurred");
//The system will now start running code inside the 'catch' block
//The system will not get here.
info("End process");
}
catch
{
info("Additional code can be run when an error occurs");
}
finally
{
Info("This block will be run whether an error occurs or not.");
}
}
The following text will display:
Start process
An error occurred
Additional code can be run when an error occurs
This block will be run whether an error occurs or not.
The finally block is typically used to dispose of objects in memory, or close open connection strings. While it is used often in .NET, I do not see it used as frequently in X++.
See this example from the class DualWriteErrorLogger.
internal static void LogErrorMessages(
RecId recordId,
DualWriteProjectConfiguration dualWriteProjectConfiguration,
str providerType,
Notes detailedErrorMessage,
Notes cdsPayload = '')
{
System.Exception ex;
UserConnection userConnection;
try
{
userConnection = new UserConnection(true);
ttsbegin;
DualWriteErrorLog dwErrorLog;
dwErrorLog.setConnection(UserConnection);
dwErrorLog.RecordIdentifier = recordId;
dwErrorLog.ProjectName = dualWriteProjectConfiguration.ProjectName;
dwErrorLog.InternalEntityName = dualWriteProjectConfiguration.InternalEntityName;
dwErrorLog.ExternalEntityName = dualWriteProjectConfiguration.ExternalEntityName;
dwErrorLog.ProviderType = providerType;
dwErrorLog.DetailedErrorMessage = detailedErrorMessage;
dwErrorLog.CDSPayload = cdsPayload;
dwErrorLog.ActivityId = guid2Str(getCurrentThreadActivityId());
dwErrorLog.doInsert();
ttscommit;
}
catch (ex)
{
//Do nothing
}
finally
{
userConnection.finalize();
}
}
Microsoft Documentation
For additional information, please see Microsoft’s documentation here.
Conclusion
Whenever possible, it is helpful for developers to write code to detect errors, and display specific instructions to the user on how to address the issue. However, not all exceptions can be anticipated. Using D365 try catch statements allows developers to provide additional context information to the end user when an error occurs. As well as allow a process to continue running without the need to restart the process.
A really comprehensive and good article on this topic Peter. Thank you.
Glad you liked it. Thank you!
In the Types Of Messages section, the description of error should be Red not yellow background in the browser.
Thanks!